Compare commits
31
Commits
cdab7a5e5f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -476,14 +476,43 @@
|
|||||||
|
|
||||||
<!-- Fieldset (single nested object) -->
|
<!-- Fieldset (single nested object) -->
|
||||||
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
||||||
<AksField
|
<template v-if="fieldsetSections.length">
|
||||||
v-for="field in visibleNestedFields"
|
<div
|
||||||
:key="field.key"
|
v-for="(section, sIndex) in fieldsetSections"
|
||||||
:schema="field"
|
:key="section.title"
|
||||||
:model-value="fieldsetValue(field)"
|
class="aksrt-field__accordion-section"
|
||||||
:form-data="formData"
|
:class="{ 'is-collapsed': !isFieldsetSectionOpen(sIndex) }"
|
||||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
>
|
||||||
/>
|
<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>
|
||||||
|
|
||||||
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
||||||
@@ -517,23 +546,38 @@
|
|||||||
animation="180"
|
animation="180"
|
||||||
>
|
>
|
||||||
<template #item="{ element, index }">
|
<template #item="{ element, index }">
|
||||||
<div class="aksrt-field__repeater-item">
|
<div class="aksrt-field__repeater-item" :class="{ 'is-collapsible': repeaterCollapsible, 'is-collapsed': repeaterCollapsible && !isRepeaterItemOpen(index) }">
|
||||||
<div class="aksrt-field__repeater-header">
|
<div
|
||||||
|
class="aksrt-field__repeater-header"
|
||||||
|
:class="{ 'is-clickable': repeaterCollapsible }"
|
||||||
|
@click="repeaterCollapsible ? toggleRepeaterItem(index) : undefined"
|
||||||
|
>
|
||||||
<div class="aksrt-field__repeater-title">
|
<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 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>
|
</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>
|
||||||
<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">
|
<template v-for="field in (schema.fields || [])" :key="field.key">
|
||||||
<AksField
|
<AksField
|
||||||
v-if="matchDependency(field.dependsOn, element)"
|
v-if="matchDependency(field.dependsOn, element)"
|
||||||
@@ -697,6 +741,7 @@ import FaIconPicker from './FaIconPicker.vue'
|
|||||||
import AksMediaField from './AksMediaField.vue'
|
import AksMediaField from './AksMediaField.vue'
|
||||||
import AksGalleryField from './AksGalleryField.vue'
|
import AksGalleryField from './AksGalleryField.vue'
|
||||||
import AksCodeEditor from './AksCodeEditor.vue'
|
import AksCodeEditor from './AksCodeEditor.vue'
|
||||||
|
import { buildFieldsetSections } from './fieldset-sections.js'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import api from '@/api'
|
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 reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
|
||||||
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
|
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)。 */
|
/** 拖拽视图:vuedraggable 以 modelValue 模式工作(内部复制后回传 update:modelValue)。 */
|
||||||
const repeaterDragList = computed({
|
const repeaterDragList = computed({
|
||||||
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
|
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
|
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) {
|
function updateFieldsetValue(key, value) {
|
||||||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||||||
? props.modelValue
|
? props.modelValue
|
||||||
@@ -1053,6 +1181,8 @@ const repeaterDraft = ref({})
|
|||||||
function createRepeaterItem() {
|
function createRepeaterItem() {
|
||||||
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
||||||
for (const field of (props.schema.fields || [])) {
|
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) {
|
if (field.default !== undefined) {
|
||||||
newItem[field.key] = field.default
|
newItem[field.key] = field.default
|
||||||
} else if (field.type === 'switch') {
|
} else if (field.type === 'switch') {
|
||||||
@@ -1082,7 +1212,16 @@ function removeRepeaterItem(index) {
|
|||||||
|
|
||||||
function updateRepeaterItem(index, fieldKey, value) {
|
function updateRepeaterItem(index, fieldKey, value) {
|
||||||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
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)
|
emit('update:modelValue', current)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,6 +1323,15 @@ function confirmRepeaterModal() {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
|
||||||
|
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
|
||||||
|
.aksrt-field--group,
|
||||||
|
.aksrt-field--repeater,
|
||||||
|
.aksrt-field--accordion,
|
||||||
|
.aksrt-field--fieldset {
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
.aksrt-field__input-wrap {
|
.aksrt-field__input-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1449,6 +1597,48 @@ function confirmRepeaterModal() {
|
|||||||
background: var(--aksrt-color-canvas);
|
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 {
|
.aksrt-field__repeater {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1477,6 +1667,47 @@ function confirmRepeaterModal() {
|
|||||||
min-width: 0;
|
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 {
|
.aksrt-field__repeater-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
color: var(--aksrt-color-text-tertiary);
|
color: var(--aksrt-color-text-tertiary);
|
||||||
|
|||||||
@@ -64,9 +64,9 @@
|
|||||||
<n-tabs type="line" animated>
|
<n-tabs type="line" animated>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-for="tab in field.tabs"
|
v-for="tab in field.tabs"
|
||||||
:key="tab.title || tab.key"
|
:key="tab.key || tab.title"
|
||||||
:name="tab.title || tab.key"
|
:name="tabName(tab)"
|
||||||
:tab="tab.title || tab.key"
|
:tab="tabName(tab)"
|
||||||
>
|
>
|
||||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||||
<AksField
|
<AksField
|
||||||
@@ -231,6 +231,18 @@ function isFieldVisible(field) {
|
|||||||
return matchDependsOn(field.dependsOn)
|
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). */
|
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||||
function isGroupVisible(group) {
|
function isGroupVisible(group) {
|
||||||
return matchDependsOn(group.dependsOn)
|
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),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ export const extensionsSection = {
|
|||||||
label: '百度推送',
|
label: '百度推送',
|
||||||
route: '/extensions/push',
|
route: '/extensions/push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'push',
|
section: 'push',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -108,16 +108,6 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'sms',
|
section: 'sms',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'users-wechat',
|
|
||||||
name: 'UsersWechat',
|
|
||||||
label: '微信通知',
|
|
||||||
route: '/users/wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
canonicalDomain: 'users',
|
|
||||||
section: 'wechat',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'users-wechat-template',
|
key: 'users-wechat-template',
|
||||||
name: 'UsersWechatTemplate',
|
name: 'UsersWechatTemplate',
|
||||||
|
|||||||
@@ -2,21 +2,16 @@ export const pushSchema = {
|
|||||||
key: 'extensions.push',
|
key: 'extensions.push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
icon: 'fa fas fa-paper-plane',
|
icon: 'fa fas fa-paper-plane',
|
||||||
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
description: '配置百度主动推送、收录通道和推送状态。',
|
||||||
defaults: {
|
defaults: {
|
||||||
baidu_push_enabled: false,
|
baidu_push_enabled: false,
|
||||||
baidu_push_token: '',
|
baidu_push_token: '',
|
||||||
baidu_push_auto: true,
|
baidu_push_auto: true,
|
||||||
baidu_push_on_publish: true,
|
baidu_push_on_publish: true,
|
||||||
baidu_push_on_update: false,
|
baidu_push_on_update: false,
|
||||||
baidu_push_batch_size: 50,
|
baidu_push_normal: true,
|
||||||
|
baidu_push_fast: false,
|
||||||
baidu_push_log_enabled: true,
|
baidu_push_log_enabled: true,
|
||||||
xzh_on: false,
|
|
||||||
xzh_appid: '',
|
|
||||||
xzh_render_tail: true,
|
|
||||||
xzh_jsonld_single: true,
|
|
||||||
xzh_jsonld_page: false,
|
|
||||||
xzh_jsonld_img: false,
|
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
@@ -25,7 +20,7 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-rocket',
|
icon: 'fa fas fa-rocket',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
||||||
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' },
|
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,21 +29,17 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-bolt',
|
icon: 'fa fas fa-bolt',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
||||||
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' },
|
||||||
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'xzh',
|
key: 'channel',
|
||||||
title: '熊掌号 / JSON-LD',
|
title: '收录通道',
|
||||||
icon: 'fa fas fa-paw',
|
icon: 'fa fas fa-inbox',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
|
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
|
||||||
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
|
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
|
||||||
{ key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,8 +47,7 @@ export const pushSchema = {
|
|||||||
title: '高级设置',
|
title: '高级设置',
|
||||||
icon: 'fa fas fa-cog',
|
icon: 'fa fas fa-cog',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
|
||||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ export const seoSchema = {
|
|||||||
ai_seo_title_prompt: '',
|
ai_seo_title_prompt: '',
|
||||||
ai_seo_keywords_prompt: '',
|
ai_seo_keywords_prompt: '',
|
||||||
ai_seo_description_prompt: '',
|
ai_seo_description_prompt: '',
|
||||||
seo_baidu_push: false,
|
|
||||||
seo_baidu_normal: false,
|
|
||||||
seo_baidu_fast: false,
|
|
||||||
seo_baidu_token: '',
|
|
||||||
seo_image_optimize: true,
|
seo_image_optimize: true,
|
||||||
seo_image_default: '',
|
seo_image_default: '',
|
||||||
seo_image_always_default: false,
|
seo_image_always_default: false,
|
||||||
@@ -95,17 +91,6 @@ export const seoSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'baidu',
|
|
||||||
title: '百度推送',
|
|
||||||
icon: 'fa fas fa-paper-plane',
|
|
||||||
fields: [
|
|
||||||
{ key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'image',
|
key: 'image',
|
||||||
title: '图片 SEO',
|
title: '图片 SEO',
|
||||||
|
|||||||
@@ -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_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: '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_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: '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)、
|
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
|
||||||
* 会员购买选项(一级/二级/续费/升级 tab)、卡密兑换与积分兑换。
|
* (普通/一级/二级 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', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '会员名称(例如“黄金会员”、“超级会员”)' },
|
||||||
|
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', default: '尊享专属特权', dependsOn: dep },
|
||||||
|
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '建议使用 <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 = {
|
export const vipSchema = {
|
||||||
key: 'payment.vip',
|
key: 'payment.vip',
|
||||||
title: 'VIP会员',
|
title: 'VIP会员',
|
||||||
icon: 'fa fas fa-gem',
|
icon: 'fa fas fa-gem',
|
||||||
description: '双级会员:等级名称、权益(下载次数与限速)、套餐、续费/升级与积分兑换。',
|
description: '双级会员:等级开关、权益(下载次数与限速)、会员商品、续费/升级与积分兑换。',
|
||||||
defaults: {
|
defaults: {
|
||||||
nav_pay_vip: true,
|
nav_pay_vip: true,
|
||||||
nav_user_pay_vip: true,
|
nav_user_pay_vip: true,
|
||||||
pay_user_vip_1_s: true,
|
pay_user_vip_1_s: true,
|
||||||
pay_user_vip_1_name: '一级会员',
|
pay_user_vip_1_name: '黄金会员',
|
||||||
pay_user_vip_1_desc: '尊享一级会员特权',
|
pay_user_vip_1_desc: '尊享专属特权',
|
||||||
pay_user_vip_1_equity: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>',
|
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: '',
|
vip_1_img_icon: '',
|
||||||
pay_user_vip_2_s: true,
|
pay_user_vip_2_s: true,
|
||||||
pay_user_vip_2_name: '二级会员',
|
pay_user_vip_2_name: '钻石会员',
|
||||||
pay_user_vip_2_desc: '尊享二级会员特权',
|
pay_user_vip_2_desc: '尊享专属特权',
|
||||||
pay_user_vip_2_equity: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>',
|
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: '',
|
vip_2_img_icon: '',
|
||||||
pay_user_vip_desc: '',
|
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||||
pay_user_vip_more: '',
|
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||||
pay_download_limit: 0,
|
pay_download_limit: 0,
|
||||||
pay_download_speed: 0,
|
pay_download_speed: 0,
|
||||||
pay_download_limit_vip_1: 0,
|
pay_download_limit_vip_1: 0,
|
||||||
@@ -29,19 +129,45 @@ export const vipSchema = {
|
|||||||
pay_download_limit_vip_2: 0,
|
pay_download_limit_vip_2: 0,
|
||||||
pay_download_speed_vip_2: 0,
|
pay_download_speed_vip_2: 0,
|
||||||
vip_1_product_s: true,
|
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: true,
|
||||||
vip_renew_desc: '',
|
vip_renew_desc: '立即续费会员,畅享VIP权益',
|
||||||
vip_renew_price_type: 'discount',
|
vip_renew_price_type: 'discount',
|
||||||
vip_renew_discount: 8,
|
vip_renew_discount: 8,
|
||||||
vip_renew_reduce: '20.00',
|
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: true,
|
||||||
vip_upgrade_desc: '',
|
vip_upgrade_desc: '升级VIP会员,享更多会员权益',
|
||||||
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
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,
|
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: '',
|
pay_vip_points_exchange_desc: '',
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
@@ -67,17 +193,16 @@ export const vipSchema = {
|
|||||||
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
||||||
{ key: 'nav_user_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_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: 'vip_level_order_notice',
|
||||||
{ 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> 列表项' },
|
label: '',
|
||||||
{ key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' },
|
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_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',
|
key: 'vip1',
|
||||||
title: '一级会员',
|
title: '一级会员',
|
||||||
|
titleKey: 'pay_user_vip_1_name',
|
||||||
|
titleFallback: '一级会员',
|
||||||
fields: [
|
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_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: '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',
|
key: 'vip2',
|
||||||
title: '二级会员',
|
title: '二级会员',
|
||||||
|
titleKey: 'pay_user_vip_2_name',
|
||||||
|
titleFallback: '二级会员',
|
||||||
fields: [
|
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_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则不限制);只对本地服务器文件有效' },
|
{ 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,110 @@ export const vipSchema = {
|
|||||||
{
|
{
|
||||||
key: 'vip1',
|
key: 'vip1',
|
||||||
title: '一级会员',
|
title: '一级会员',
|
||||||
|
titleKey: 'pay_user_vip_1_name',
|
||||||
|
titleFallback: '一级会员',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
...vipLevelTabFields(1),
|
||||||
{
|
|
||||||
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',
|
key: 'vip2',
|
||||||
title: '二级会员',
|
title: '二级会员',
|
||||||
|
titleKey: 'pay_user_vip_2_name',
|
||||||
|
titleFallback: '二级会员',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
...vipLevelTabFields(2),
|
||||||
{
|
|
||||||
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: 'renew',
|
key: 'renew',
|
||||||
title: '会员续费',
|
title: '会员续费',
|
||||||
fields: [
|
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',
|
||||||
|
default: '立即续费会员,畅享VIP权益',
|
||||||
|
dependsOn: { key: 'vip_renew', value: true },
|
||||||
|
help: '续费会员的一句话简介',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'vip_renew_price_type',
|
key: 'vip_renew_price_type',
|
||||||
label: '续费价格类型',
|
label: '续费价格类型',
|
||||||
type: 'radio',
|
type: 'select',
|
||||||
default: 'discount',
|
default: 'discount',
|
||||||
dependsOn: { key: 'vip_renew', value: true },
|
dependsOn: { key: 'vip_renew', value: true },
|
||||||
options: [
|
options: [
|
||||||
{ label: '相同价格', value: 'same' },
|
{ label: '相同价格', value: 'same' },
|
||||||
{ label: '折扣价格', value: 'discount' },
|
{ label: '折扣价格', value: 'discount' },
|
||||||
{ label: '优惠立减', value: 'reduce' },
|
{ 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 +371,50 @@ export const vipSchema = {
|
|||||||
title: '会员升级',
|
title: '会员升级',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
|
{ 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', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介' },
|
||||||
{
|
{
|
||||||
key: 'vip_upgrade_product',
|
key: 'vip_upgrade_product',
|
||||||
label: '升级产品',
|
label: '升级价格',
|
||||||
type: 'textarea',
|
type: 'accordion',
|
||||||
default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
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 },
|
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 +427,71 @@ export const vipSchema = {
|
|||||||
title: '积分兑换会员',
|
title: '积分兑换会员',
|
||||||
icon: 'fa fas fa-database',
|
icon: 'fa fas fa-database',
|
||||||
fields: [
|
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',
|
key: 'pay_vip_points_exchange_product',
|
||||||
label: '兑换套餐',
|
label: '积分兑换明细',
|
||||||
type: 'textarea',
|
type: 'group',
|
||||||
default: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
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 },
|
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',
|
||||||
|
default: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||||
|
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||||
|
help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 { verifySchema } from './verify.js'
|
||||||
import { levelSchema } from './level.js'
|
import { levelSchema } from './level.js'
|
||||||
import { badgeSchema } from './badge.js'
|
import { badgeSchema } from './badge.js'
|
||||||
import { wechatSchema } from './wechat.js'
|
|
||||||
import { reportSchema } from './report.js'
|
import { reportSchema } from './report.js'
|
||||||
import { smsSchema } from './sms.js'
|
import { smsSchema } from './sms.js'
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export const usersSchemas = {
|
|||||||
verify: verifySchema,
|
verify: verifySchema,
|
||||||
level: levelSchema,
|
level: levelSchema,
|
||||||
badge: badgeSchema,
|
badge: badgeSchema,
|
||||||
wechat: wechatSchema,
|
|
||||||
report: reportSchema,
|
report: reportSchema,
|
||||||
sms: smsSchema,
|
sms: smsSchema,
|
||||||
}
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export const wechatSchema = {
|
|
||||||
key: 'users.wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
icon: 'fa fab fa-weixin',
|
|
||||||
description: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
defaults: {
|
|
||||||
wechat_notify_enabled: false,
|
|
||||||
wechat_app_id: '',
|
|
||||||
wechat_app_secret: '',
|
|
||||||
wechat_template_id: '',
|
|
||||||
wechat_token: '',
|
|
||||||
wechat_encoding_aes_key: '',
|
|
||||||
wechat_notify_comment: true,
|
|
||||||
wechat_notify_reply: true,
|
|
||||||
wechat_notify_system: true,
|
|
||||||
wechat_bind_entry: true,
|
|
||||||
},
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
key: 'account',
|
|
||||||
title: '公众号配置',
|
|
||||||
icon: 'fa fab fa-weixin',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_notify_enabled', label: '启用微信通知', type: 'switch', default: false, help: '开启后用户可绑定微信接收通知' },
|
|
||||||
{ key: 'wechat_app_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_template_id', label: '模板消息 ID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在微信公众平台申请的模板消息 ID' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'verify',
|
|
||||||
title: '服务器验证',
|
|
||||||
icon: 'fa fas fa-check-circle',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_token', label: 'Token', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '微信公众平台服务器配置中的 Token' },
|
|
||||||
{ key: 'wechat_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'triggers',
|
|
||||||
title: '通知触发器',
|
|
||||||
icon: 'fa fas fa-bell',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_notify_comment', label: '新评论通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_notify_reply', label: '评论回复通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_notify_system', label: '系统通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_bind_entry', label: '显示绑定入口', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在用户中心显示微信绑定/解绑按钮' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -709,8 +709,9 @@
|
|||||||
color: var(--color-primary, #f04494);
|
color: var(--color-primary, #f04494);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||||
.widget a.but {
|
.widget a.but,
|
||||||
|
.widget a.img-badge {
|
||||||
color: var(--this-color, #fff);
|
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
|
||||||
+48
-70
@@ -1095,7 +1095,7 @@ strong, b {
|
|||||||
|
|
||||||
/* gradient modifiers (legacy .jb-*) */
|
/* gradient modifiers (legacy .jb-*) */
|
||||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
.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-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||||
@@ -2714,27 +2714,29 @@ main.container,
|
|||||||
* shell anchoring plus the account action-row geometry.
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||||
.navbar-top .aksrt-navbar-actions {
|
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||||
display: flex;
|
* override only zeroes the horizontal margins, so every right-cluster group
|
||||||
align-items: center;
|
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||||
gap: 10px;
|
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||||
min-height: 50px;
|
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||||
margin-top: 0;
|
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||||
margin-bottom: 0;
|
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||||
padding: 0;
|
* 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-actions > *,
|
.navbar-top .aksrt-navbar-account {
|
||||||
.navbar-top .aksrt-navbar-account,
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
.navbar-top .aksrt-navbar-account > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -2747,39 +2749,6 @@ main.container,
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account {
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: .3em 1em !important;
|
|
||||||
border-radius: var(--radius-btn, 4px);
|
|
||||||
font-size: .8em;
|
|
||||||
line-height: 1.44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-nav-icon,
|
.navbar-top .aksrt-nav-icon,
|
||||||
.navbar-top .aksrt-nav-avatar {
|
.navbar-top .aksrt-nav-avatar {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -2812,33 +2781,34 @@ main.container,
|
|||||||
object-fit: cover;
|
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;
|
flex: 0 0 29px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
vertical-align: middle;
|
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;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||||
* the absolute panel's static position beside the compact CTA. */
|
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||||
position: relative;
|
* would move that static position to the flex container's cross-start. */
|
||||||
align-self: stretch;
|
.navbar-top .navbar-but > .hover-show {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
width: max-content;
|
|
||||||
max-width: calc(100vw - 24px);
|
|
||||||
margin: 0;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||||
@@ -2886,11 +2856,19 @@ main.container,
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The later .but compatibility layer otherwise replaces the legacy payvip surface. */
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
.aksrt-header-userbox .pay-vip.but.payvip-icon {
|
* 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%);
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
color: #ece0e0;
|
color: #ece0e0;
|
||||||
--this-color: #ece0e0;
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
|
|||||||
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
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
|
|
||||||
/* gradient modifiers (legacy .jb-*) */
|
/* gradient modifiers (legacy .jb-*) */
|
||||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
.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-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||||
|
|||||||
@@ -7,27 +7,29 @@
|
|||||||
* shell anchoring plus the account action-row geometry.
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||||
.navbar-top .aksrt-navbar-actions {
|
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||||
display: flex;
|
* override only zeroes the horizontal margins, so every right-cluster group
|
||||||
align-items: center;
|
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||||
gap: 10px;
|
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||||
min-height: 50px;
|
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||||
margin-top: 0;
|
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||||
margin-bottom: 0;
|
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||||
padding: 0;
|
* 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-actions > *,
|
.navbar-top .aksrt-navbar-account {
|
||||||
.navbar-top .aksrt-navbar-account,
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
.navbar-top .aksrt-navbar-account > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -40,39 +42,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account {
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: .3em 1em !important;
|
|
||||||
border-radius: var(--radius-btn, 4px);
|
|
||||||
font-size: .8em;
|
|
||||||
line-height: 1.44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-nav-icon,
|
.navbar-top .aksrt-nav-icon,
|
||||||
.navbar-top .aksrt-nav-avatar {
|
.navbar-top .aksrt-nav-avatar {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -105,33 +74,34 @@
|
|||||||
object-fit: cover;
|
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;
|
flex: 0 0 29px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
vertical-align: middle;
|
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;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||||
* the absolute panel's static position beside the compact CTA. */
|
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||||
position: relative;
|
* would move that static position to the flex container's cross-start. */
|
||||||
align-self: stretch;
|
.navbar-top .navbar-but > .hover-show {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
width: max-content;
|
|
||||||
max-width: calc(100vw - 24px);
|
|
||||||
margin: 0;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||||
@@ -179,11 +149,19 @@
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The later .but compatibility layer otherwise replaces the legacy payvip surface. */
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
.aksrt-header-userbox .pay-vip.but.payvip-icon {
|
* 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%);
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
color: #ece0e0;
|
color: #ece0e0;
|
||||||
--this-color: #ece0e0;
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
|
|||||||
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
|
|||||||
font-size: 13px;
|
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 {
|
.auth-footer-link {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|||||||
@@ -50,8 +50,9 @@
|
|||||||
color: var(--color-primary, #f04494);
|
color: var(--color-primary, #f04494);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||||
.widget a.but {
|
.widget a.but,
|
||||||
|
.widget a.img-badge {
|
||||||
color: var(--this-color, #fff);
|
color: var(--this-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,3 +101,86 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 会员弹窗骨架屏(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;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.aksrt-vip-modal-skeleton__pane {
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-4
@@ -6838,7 +6838,7 @@ badge.top,
|
|||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .navbar-but > .but {
|
.navbar-top .navbar-but .but {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
padding: 0.3em 1em !important;
|
padding: 0.3em 1em !important;
|
||||||
margin: 0.4em;
|
margin: 0.4em;
|
||||||
@@ -9655,11 +9655,16 @@ img.alignleft {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap
|
/* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
|
||||||
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高;
|
* 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
|
||||||
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */
|
* "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
|
||||||
|
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
|
||||||
|
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
|
||||||
.user-card .card-content .img-badge {
|
.user-card .card-content .img-badge {
|
||||||
line-height: 1.42857143;
|
line-height: 1.42857143;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: auto;
|
||||||
|
text-rendering: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card name .display-name {
|
.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
|
||||||
@@ -478,18 +478,6 @@
|
|||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
background: var(--color-primary-softer);
|
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 {
|
.binding-status {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -304,12 +304,24 @@
|
|||||||
body.uc-modal-open {
|
body.uc-modal-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.btn-block {
|
/* Scoped to the user-center content shell. These are the credits panel's own
|
||||||
display: block;
|
* button skins, and every element they target renders inside .uc-content (the
|
||||||
width: 100%;
|
* panels and their modals are all emitted through templates/parts/user-center/
|
||||||
}
|
* panel.php, which templates/pages/page-profile.php wraps in .uc-content).
|
||||||
.but.jb-blue,
|
*
|
||||||
.but.b-blue-2 {
|
* 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;
|
background: #3b82f6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -317,18 +329,18 @@ body.uc-modal-open {
|
|||||||
padding: 0.45em 1em;
|
padding: 0.45em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.but.c-blue-2.hollow,
|
.uc-content .but.c-blue-2.hollow,
|
||||||
.but.c-green.hollow {
|
.uc-content .but.c-green.hollow {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.4em 0.9em;
|
padding: 0.4em 0.9em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.but.c-blue-2 {
|
.uc-content .but.c-blue-2 {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
.but.c-green {
|
.uc-content .but.c-green {
|
||||||
color: #16a34a;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,8 +264,6 @@
|
|||||||
.aksrt-account-settings__section--social .binding-captcha { margin-bottom: 12px; }
|
.aksrt-account-settings__section--social .binding-captcha { margin-bottom: 12px; }
|
||||||
.aksrt-account-list--social .binding-item { border: 0; }
|
.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 .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 { 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-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
|
||||||
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
|
.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 {
|
.uc-sidebar__pair {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -684,7 +665,8 @@ body.aksrt-user-center .uc-content,
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uc-summary-card__body > i {
|
.uc-summary-card__body > i,
|
||||||
|
.uc-summary-card__body > svg {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,14 +698,26 @@ body.aksrt-user-center .uc-content,
|
|||||||
|
|
||||||
.uc-center-card strong { font-size: 13px; }
|
.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 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--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-center-card--revenue .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); }
|
||||||
|
|
||||||
.uc-service-grid {
|
/* Services / settings grids, ported from the reference theme sidebar:
|
||||||
display: grid;
|
four per row, no tinted tile behind the icon. */
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
.user-icon-but-box > a {
|
||||||
gap: 8px;
|
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; }
|
.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; }
|
.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; } }
|
@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);
|
|
||||||
}
|
|
||||||
+103
-7
@@ -17,6 +17,15 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 服务端已按 post 级 HTML 净化的富文本字段(more / 卡密说明)走 innerHTML;
|
||||||
|
// node() 的 textContent 会把标签当字面文本显示。
|
||||||
|
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) {
|
function imageNode(src, className) {
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
var image = document.createElement("img");
|
var image = document.createElement("img");
|
||||||
@@ -115,7 +124,8 @@
|
|||||||
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
||||||
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
||||||
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
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, node("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 || ""));
|
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));
|
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
||||||
append(label, product);
|
append(label, product);
|
||||||
@@ -179,7 +189,7 @@
|
|||||||
box.setAttribute("data-condition", "==");
|
box.setAttribute("data-condition", "==");
|
||||||
box.setAttribute("data-value", "card_pass");
|
box.setAttribute("data-value", "card_pass");
|
||||||
if (defaultMethod !== "card_pass") box.style.display = "none";
|
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 ? "请输入卡密" : "请输入卡号和密码"));
|
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
||||||
if (!cardPass.only_password) {
|
if (!cardPass.only_password) {
|
||||||
var cardInput = node("input", "form-control mb6");
|
var cardInput = node("input", "form-control mb6");
|
||||||
@@ -492,7 +502,7 @@
|
|||||||
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
||||||
append(products, pointsLink);
|
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);
|
renderMethods(products, methods, cardPass, wallet);
|
||||||
var actionName = actionKey(action);
|
var actionName = actionKey(action);
|
||||||
var actionAllowed = allowed[actionName] === true;
|
var actionAllowed = allowed[actionName] === true;
|
||||||
@@ -519,8 +529,6 @@
|
|||||||
append(modal, tabs);
|
append(modal, tabs);
|
||||||
append(modal, content);
|
append(modal, content);
|
||||||
|
|
||||||
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
|
|
||||||
|
|
||||||
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
|
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
|
||||||
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
||||||
applyVipMethodVisibility(pane);
|
applyVipMethodVisibility(pane);
|
||||||
@@ -698,6 +706,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) {
|
function vipPay(trigger) {
|
||||||
var level = trigger
|
var level = trigger
|
||||||
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
||||||
@@ -706,11 +754,11 @@
|
|||||||
url: "/pay/vip/modal",
|
url: "/pay/vip/modal",
|
||||||
query: { vip_level: level },
|
query: { vip_level: level },
|
||||||
className: "aksrt-vip-modal-shell",
|
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",
|
telemetry: "vip",
|
||||||
kind: "vip",
|
kind: "vip",
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
cacheTtl: 15000,
|
cacheTtl: 60000,
|
||||||
cacheTags: ["vip"],
|
cacheTags: ["vip"],
|
||||||
renderData: function (data, session) {
|
renderData: function (data, session) {
|
||||||
renderModalView(session.content, data);
|
renderModalView(session.content, data);
|
||||||
@@ -728,6 +776,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) {
|
function onDocClick(event) {
|
||||||
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||||
if (!trigger) return;
|
if (!trigger) return;
|
||||||
@@ -768,6 +863,7 @@
|
|||||||
if (window.__aksrtPayVipBound) return;
|
if (window.__aksrtPayVipBound) return;
|
||||||
window.__aksrtPayVipBound = true;
|
window.__aksrtPayVipBound = true;
|
||||||
document.addEventListener("click", onDocClick, true);
|
document.addEventListener("click", onDocClick, true);
|
||||||
|
bindPrefetch();
|
||||||
bindPointsModal();
|
bindPointsModal();
|
||||||
bindPointsEntry();
|
bindPointsEntry();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
|
|||||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||||
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
|
||||||
|
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
|
||||||
|
}
|
||||||
if (class_exists('\AKSRT\Core\SEOHead')) {
|
if (class_exists('\AKSRT\Core\SEOHead')) {
|
||||||
\AKSRT\Core\SEOHead::get_instance()->init();
|
\AKSRT\Core\SEOHead::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
|
|||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_normalize_header_cta_label')) :
|
|
||||||
/**
|
|
||||||
* Normalize CTA label HTML: map FA5 classes and keep a single leading icon.
|
|
||||||
*/
|
|
||||||
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fa fas fa-pen'): string {
|
|
||||||
$label = trim($label);
|
|
||||||
if ($label === '') {
|
|
||||||
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stripos($label, '<i') === false) {
|
|
||||||
// No space between icon and label — spacing via CSS margin (legacy .but > .fa).
|
|
||||||
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i><span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
|
|
||||||
} else {
|
|
||||||
$label = preg_replace(
|
|
||||||
'/(<\/i>)\s*([^<].*)$/u',
|
|
||||||
'$1<span class="navbar-cta-text">$2</span>',
|
|
||||||
$label
|
|
||||||
) ?? $label;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $label;
|
|
||||||
}
|
|
||||||
endif;
|
|
||||||
|
|
||||||
if (!function_exists('aksrt_render_new_add_btns')) :
|
if (!function_exists('aksrt_render_new_add_btns')) :
|
||||||
/**
|
/**
|
||||||
* Multi-type publish button (legacy newadd-btns).
|
* Publish chip (legacy newadd-btns, single action).
|
||||||
|
*
|
||||||
|
* Mirrors the single-button branch of the reference zib_get_new_add_btns():
|
||||||
|
* the paint classes ride on the anchor itself and the label is emitted
|
||||||
|
* verbatim. Only the post action exists, so there is no dropdown branch.
|
||||||
*
|
*
|
||||||
* @param string[] $types
|
* @param string[] $types
|
||||||
*/
|
*/
|
||||||
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$actions = aksrt_get_new_add_actions($types);
|
$actions = aksrt_get_new_add_actions($types);
|
||||||
if (!$actions) {
|
if (!$actions) {
|
||||||
// Last resort: keep a write-post entry so the top-nav chip is never blank.
|
return '';
|
||||||
$fallback = function_exists('aksrt_get_new_post_url') ? (string) aksrt_get_new_post_url() : '';
|
|
||||||
if ($fallback === '') {
|
|
||||||
$fallback = admin_url('post-new.php');
|
|
||||||
}
|
|
||||||
$actions = [[
|
|
||||||
'type' => 'post',
|
|
||||||
'url' => $fallback,
|
|
||||||
'label' => __('写文章', 'aksrt'),
|
|
||||||
'icon' => 'fa fas fa-file-alt',
|
|
||||||
]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$a = $actions[0];
|
||||||
if ($label === '') {
|
if ($label === '') {
|
||||||
$label = __('发布', 'aksrt');
|
$label = __('发布', 'aksrt');
|
||||||
}
|
}
|
||||||
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
|
|
||||||
|
|
||||||
// Split paint classes vs wrapper tokens.
|
return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
|
||||||
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
. $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
|
||||||
$cta_tokens = [];
|
|
||||||
$wrap_tokens = ['newadd-btns'];
|
|
||||||
foreach ($class_tokens as $token) {
|
|
||||||
if ($token === 'newadd-btns') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($token === 'btn-primary' || $token === 'but' || $token === 'navbar-cta' || $token === 'radius' || $token === 'main-radius' || $token === 'mini-radius' || str_starts_with($token, 'jb-') || str_starts_with($token, 'b-')) {
|
|
||||||
$cta_tokens[] = $token;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$wrap_tokens[] = $token;
|
|
||||||
}
|
|
||||||
if (!$cta_tokens) {
|
|
||||||
$cta_tokens[] = 'jb-blue';
|
|
||||||
}
|
|
||||||
if (!in_array('but', $cta_tokens, true)) {
|
|
||||||
$cta_tokens[] = 'but';
|
|
||||||
}
|
|
||||||
$cta_class = trim(implode(' ', array_unique($cta_tokens)));
|
|
||||||
|
|
||||||
if (count($actions) === 1) {
|
|
||||||
$a = $actions[0];
|
|
||||||
return '<a rel="nofollow" class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' ' . $cta_class . ' navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
|
|
||||||
. $label_html
|
|
||||||
. '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacy: span.newadd-btns.hover-show > toggle chip + .hover-show-con.drop-newadd
|
|
||||||
// Paint classes (jb-*, but, radius) MUST live on the toggle — wrapper is layout-only
|
|
||||||
// (background forced transparent). Bare navbar-but has no .navbar-but-cluster shell.
|
|
||||||
// data-aksrt-newadd: JS may portal menu to body so chip width cannot track menu.
|
|
||||||
$html = '<span class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' hover-show')) . '" data-aksrt-newadd="1">';
|
|
||||||
$html .= '<span class="newadd-btns__toggle navbar-cta ' . esc_attr($cta_class) . '">' . $label_html
|
|
||||||
. '<i class="fa fas fa-caret-down" aria-hidden="true"></i></span>';
|
|
||||||
$html .= '<div class="hover-show-con dropdown-menu drop-newadd" role="menu">';
|
|
||||||
foreach ($actions as $a) {
|
|
||||||
$html .= '<a rel="nofollow" class="btn-newadd newadd-btns__item" role="menuitem" href="' . esc_url($a['url']) . '">';
|
|
||||||
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
|
|
||||||
. '<text>' . esc_html($a['label']) . '</text>';
|
|
||||||
$html .= '</a>';
|
|
||||||
}
|
|
||||||
$html .= '</div></span>';
|
|
||||||
return $html;
|
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
*/
|
*/
|
||||||
function aksrt_render_header_menu_buttons(): string {
|
function aksrt_render_header_menu_buttons(): string {
|
||||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
||||||
$cfg = [
|
$enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
|
||||||
'enabled' => !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']),
|
$text = trim((string) ($settings['nav_newposts_text'] ?? ''));
|
||||||
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))),
|
$pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
|
||||||
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']),
|
$skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
|
||||||
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
|
|
||||||
];
|
|
||||||
$html = '';
|
$html = '';
|
||||||
if (!empty($cfg['enabled'])) {
|
if ($enabled) {
|
||||||
// legacy default: but nowave jb-blue (+ radius).
|
// legacy default: but nowave jb-blue (+ radius).
|
||||||
$class = 'but nowave navbar-newposts';
|
$class = 'but nowave ' . $skin;
|
||||||
$skin = trim((string) ($cfg['class'] ?? ''));
|
|
||||||
if ($skin === '') {
|
|
||||||
$skin = 'jb-blue';
|
|
||||||
}
|
|
||||||
$class .= ' ' . $skin;
|
|
||||||
$pill = !empty($cfg['radius_pill']);
|
|
||||||
if ($pill && !str_contains($class, 'radius')) {
|
if ($pill && !str_contains($class, 'radius')) {
|
||||||
$class .= ' radius';
|
$class .= ' radius';
|
||||||
}
|
}
|
||||||
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt'));
|
$label = $text !== '' ? $text : __('发布', 'aksrt');
|
||||||
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
||||||
if ($btn !== '') {
|
if ($btn !== '') {
|
||||||
$html .= $btn;
|
$html .= $btn;
|
||||||
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
$show_vip = false;
|
$show_vip = false;
|
||||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
||||||
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip']);
|
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip'])
|
||||||
|
&& !\AKSRT\Settings\Settings::close_sign();
|
||||||
}
|
}
|
||||||
if ($show_vip) {
|
if ($show_vip) {
|
||||||
$is_vip = false;
|
$is_vip = false;
|
||||||
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
if (method_exists($svc, 'render_vip_card')) {
|
if (method_exists($svc, 'render_vip_card')) {
|
||||||
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
||||||
if ($cards !== '') {
|
if ($cards !== '') {
|
||||||
$hover = '<div class="hover-show-con sub-menu sub-vip-card">' . $cards . '</div>';
|
$hover = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// legacy: span.hover-show wraps button + .hover-show-con.sub-vip-card
|
// legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
|
||||||
$html .= '<span class="hover-show inline-block navbar-vip-hover">';
|
$html .= '<span class="hover-show inline-block">';
|
||||||
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"'
|
$html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
|
||||||
. ($user_id > 0 ? ' vip-level="1"' : '') . '>';
|
. '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
. esc_html__('开通会员', 'aksrt') . '</a>' . $hover . '</span>';
|
||||||
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
|
|
||||||
$html .= '</a>' . $hover . '</span>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
||||||
if ($signed) {
|
if ($signed) {
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
// 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
$class . ' is-done',
|
$class,
|
||||||
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
. '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
. esc_html__('已签到', 'aksrt')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
@@ -137,6 +137,8 @@ if (!function_exists('aksrt_header_user_wallet_mini')) :
|
|||||||
|
|
||||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||||
$box = '';
|
$box = '';
|
||||||
|
$balance_icon = aksrt_icon_markup('zibsvg-money-color-2', 'em14');
|
||||||
|
$points_icon = aksrt_icon_markup('zibsvg-points-color', 'em14');
|
||||||
|
|
||||||
if ($balance_on) {
|
if ($balance_on) {
|
||||||
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
@@ -144,7 +146,7 @@ if (!function_exists('aksrt_header_user_wallet_mini')) :
|
|||||||
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
. '<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">'
|
. '<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)))
|
. esc_html(aksrt_cut_count((float) aksrt_user_balance_decimal($user_id)))
|
||||||
. '</span><i class="fa fas fa-money-bill-wave em14 c-blue-2" aria-hidden="true"></i></div></a>';
|
. '</span>' . $balance_icon . '</div></a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($points_on) {
|
if ($points_on) {
|
||||||
@@ -154,7 +156,7 @@ if (!function_exists('aksrt_header_user_wallet_mini')) :
|
|||||||
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
. '<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">'
|
. '<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)))
|
. esc_html(aksrt_cut_count(aksrt_user_points($user_id)))
|
||||||
. '</span><i class="fa fas fa-coins em14 c-yellow" aria-hidden="true"></i></div></a>';
|
. '</span>' . $points_icon . '</div></a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
||||||
@@ -230,11 +232,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 几何对齐参考主题 CTA(通栏、radius4)——不是顶导 navbar-cta chips。
|
// 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
|
||||||
return '<div class="mt10 em09" style="padding:2px;">'
|
return '<div class="mt10 em09" style="padding:2px;">'
|
||||||
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;" vip-level="1">'
|
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
|
||||||
. '<i class="fa fas fa-gem em12 mr6" aria-hidden="true"></i>'
|
. '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>'
|
. esc_html__('开通会员 尊享会员权益', 'aksrt')
|
||||||
. '</a></div>';
|
. '</a></div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('aksrt_brand_icon_map')) :
|
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>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
|
|||||||
return $map;
|
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 = [
|
$map = [
|
||||||
'github' => 'fa fab fa-github',
|
'github' => 'fa fab fa-github',
|
||||||
'google' => 'fa fab fa-google',
|
'google' => 'zibsvg-google-color',
|
||||||
'x' => 'fa fab fa-twitter',
|
'x' => 'fa fab fa-twitter',
|
||||||
'twitter' => 'fa fab fa-twitter',
|
'twitter' => 'fa fab fa-twitter',
|
||||||
'email' => 'fa fas fa-envelope',
|
'email' => 'fa fas fa-envelope',
|
||||||
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
|
|||||||
'instagram' => 'fa fab fa-instagram',
|
'instagram' => 'fa fab fa-instagram',
|
||||||
'web' => 'fa fas fa-globe',
|
'web' => 'fa fas fa-globe',
|
||||||
'globe' => 'fa fas fa-globe',
|
'globe' => 'fa fas fa-globe',
|
||||||
'gitee' => 'fa fas fa-code-branch',
|
'gitee' => 'zibsvg-gitee',
|
||||||
'baidu' => 'fa fas fa-paw',
|
'baidu' => 'fa fas fa-paw',
|
||||||
'alipay' => 'fa fab fa-alipay',
|
'alipay' => 'fa fab fa-alipay',
|
||||||
];
|
];
|
||||||
@@ -127,10 +129,10 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('aksrt_brand_icon_class')) :
|
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.
|
* @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
|
||||||
* @return string e.g. 'fa fab fa-github', or '' if unknown.
|
* @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
|
||||||
*/
|
*/
|
||||||
function aksrt_brand_icon_class(string $name): string {
|
function aksrt_brand_icon_class(string $name): string {
|
||||||
$raw = trim($name);
|
$raw = trim($name);
|
||||||
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
|
|||||||
return '';
|
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).
|
// 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)) {
|
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.).
|
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
|
||||||
@@ -164,14 +172,21 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('aksrt_brand_icon')) :
|
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.
|
* @param array|string $extra_classes Optional extra classes.
|
||||||
* @return string Safe HTML or empty string when unknown.
|
* @return string Safe HTML or empty string when unknown.
|
||||||
*/
|
*/
|
||||||
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
||||||
$class = aksrt_brand_icon_class($name);
|
$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 === '') {
|
if ($class === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">'
|
$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>';
|
. '<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')) {
|
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'));
|
$body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
||||||
if ($social !== '') {
|
|
||||||
$body .= '<div class="social_loginbar">' . $social . '</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$body .= '</div>';
|
$body .= '</div>';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -286,11 +286,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
|
|
||||||
$con = '<div class="text-center">' . $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')) {
|
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'));
|
$con .= (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>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +299,12 @@ if (!function_exists('aksrt_menu_button')) :
|
|||||||
/**
|
/**
|
||||||
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
||||||
*
|
*
|
||||||
|
* The reference splits the cluster into three floated .navbar-form
|
||||||
|
* groups (publish/VIP, theme + language, account row). Each group
|
||||||
|
* keeps its own .navbar-form padding, which is what produces the
|
||||||
|
* reference inter-cluster spacing, so the groups are emitted
|
||||||
|
* separately instead of being merged into one flex container.
|
||||||
|
*
|
||||||
* @param array<string,mixed> $ctx
|
* @param array<string,mixed> $ctx
|
||||||
*/
|
*/
|
||||||
function aksrt_menu_button(array $ctx): void {
|
function aksrt_menu_button(array $ctx): void {
|
||||||
@@ -371,14 +373,24 @@ if (!function_exists('aksrt_menu_button')) :
|
|||||||
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
||||||
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||||
. $notification_but . $sign_but . $search_but . '</ul>';
|
. $notification_but . $sign_but . $search_but . '</ul>';
|
||||||
$button = '<div class="aksrt-navbar-actions navbar-form navbar-right navbar-but">'
|
|
||||||
. $actions . $utility_buttons . $account . '</div>';
|
// Reference cluster split: publish/VIP, theme + language, account row.
|
||||||
|
$button = '';
|
||||||
|
if ($actions !== '') {
|
||||||
|
$button = '<div class="navbar-form navbar-right navbar-but">' . $actions . '</div>';
|
||||||
|
}
|
||||||
|
if ($utility_buttons !== '') {
|
||||||
|
$button .= '<div class="navbar-form navbar-right flex ac">' . $utility_buttons . '</div>';
|
||||||
|
}
|
||||||
|
$right_but = '<div class="navbar-form navbar-right'
|
||||||
|
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||||
|
. $account . '</div>';
|
||||||
|
|
||||||
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
||||||
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
||||||
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
||||||
|
|
||||||
$html = $fold . $button;
|
$html = $fold . $button . $right_but;
|
||||||
|
|
||||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ require __DIR__ . '/sidebar-vip.php';
|
|||||||
require __DIR__ . '/sidebar-commerce.php';
|
require __DIR__ . '/sidebar-commerce.php';
|
||||||
require __DIR__ . '/sidebar-creator.php';
|
require __DIR__ . '/sidebar-creator.php';
|
||||||
require __DIR__ . '/sidebar-nav.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'); ?>">
|
<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>
|
</button>
|
||||||
@@ -26,10 +26,14 @@ foreach ((array) ($oauth_data['bindings'] ?? []) as $binding) {
|
|||||||
if (!$key) {
|
if (!$key) {
|
||||||
continue;
|
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] = [
|
$binding_rows[$key] = [
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||||
'icon' => $binding['icon'] ?? '',
|
'icon' => $binding['icon'] ?? '',
|
||||||
|
'icon_type' => $icon_type,
|
||||||
'is_bound' => true,
|
'is_bound' => true,
|
||||||
'url' => '',
|
'url' => '',
|
||||||
];
|
];
|
||||||
@@ -39,10 +43,12 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
|||||||
if (!$key) {
|
if (!$key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||||
$binding_rows[$key] = [
|
$binding_rows[$key] = [
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||||
'icon' => $binding['icon'] ?? '',
|
'icon' => $binding['icon'] ?? '',
|
||||||
|
'icon_type' => $icon_type,
|
||||||
'is_bound' => false,
|
'is_bound' => false,
|
||||||
'url' => $binding['start_url'] ?? $binding['url'] ?? '',
|
'url' => $binding['start_url'] ?? $binding['url'] ?? '',
|
||||||
];
|
];
|
||||||
@@ -64,7 +70,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
|||||||
<?php foreach ($binding_rows as $binding) : ?>
|
<?php foreach ($binding_rows as $binding) : ?>
|
||||||
<?php if (!empty($binding['is_bound'])) : ?>
|
<?php if (!empty($binding['is_bound'])) : ?>
|
||||||
<div class="aksrt-account-row binding-item">
|
<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="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||||
<span class="binding-status">
|
<span class="binding-status">
|
||||||
<i class="fa fas fa-check-circle badge__icon" aria-hidden="true"></i>
|
<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>
|
</div>
|
||||||
<?php else : ?>
|
<?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']); ?>">
|
<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="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="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>
|
<span class="but hollow c-blue aksrt-account-row__action"><?php esc_html_e('绑定', 'aksrt'); ?></span>
|
||||||
|
|||||||
@@ -20,23 +20,26 @@ try {
|
|||||||
$wallet = '0.00';
|
$wallet = '0.00';
|
||||||
}
|
}
|
||||||
$points = max(0, (int) ($credit_balance ?? 0));
|
$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">
|
<div class="uc-sidebar__pair uc-sidebar__pair--assets">
|
||||||
<?php if ($show_wallet) : ?>
|
<?php if ($show_wallet) : ?>
|
||||||
<a class="uc-summary-card uc-summary-card--wallet" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
<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">
|
<span class="uc-summary-card__body">
|
||||||
<strong data-aksrt-wallet-value><?php echo esc_html($wallet); ?></strong>
|
<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>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($show_credits) : ?>
|
<?php if ($show_credits) : ?>
|
||||||
<a class="uc-summary-card uc-summary-card--points" href="<?php echo esc_url($assets_url); ?>" data-tab="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">
|
<span class="uc-summary-card__body">
|
||||||
<strong data-aksrt-points-value data-credits-value><?php echo esc_html(aksrt_format_points($points)); ?></strong>
|
<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>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -13,20 +13,23 @@ $revenue_url = isset($dashboard_tabs['revenue']) ? aksrt_get_user_center_tab_url
|
|||||||
if ($creation_url === '' && $revenue_url === '') {
|
if ($creation_url === '' && $revenue_url === '') {
|
||||||
return;
|
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">
|
<div class="uc-sidebar__pair uc-sidebar__pair--centers">
|
||||||
<?php if ($creation_url !== '') : ?>
|
<?php if ($creation_url !== '') : ?>
|
||||||
<a class="uc-center-card uc-center-card--creation" href="<?php echo esc_url($creation_url); ?>" data-tab="creator">
|
<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>
|
<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>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($revenue_url !== '') : ?>
|
<?php if ($revenue_url !== '') : ?>
|
||||||
<a class="uc-center-card uc-center-card--revenue" href="<?php echo esc_url($revenue_url); ?>" data-tab="revenue">
|
<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>
|
<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>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* User center sidebar: services and account settings.
|
* 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
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -17,22 +20,20 @@ $orders_url = $orders_available ? (string) ($orders_url ?? '') : '';
|
|||||||
$services = array_values(array_filter([
|
$services = array_values(array_filter([
|
||||||
[
|
[
|
||||||
'label' => __('我的等级', 'aksrt'),
|
'label' => __('我的等级', 'aksrt'),
|
||||||
'icon' => 'chart-line',
|
'icon' => 'trend-color',
|
||||||
'tone' => 'level',
|
|
||||||
'url' => $level_url,
|
'url' => $level_url,
|
||||||
|
'tab' => 'level',
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'label' => __('官方认证', 'aksrt'),
|
'label' => __('官方认证', 'aksrt'),
|
||||||
'icon' => 'id-badge',
|
'icon' => 'user-auth',
|
||||||
'tone' => 'auth',
|
|
||||||
'url' => $auth_url,
|
'url' => $auth_url,
|
||||||
'tab' => 'auth',
|
'tab' => 'auth',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('我的订单', 'aksrt'),
|
'label' => __('我的订单', 'aksrt'),
|
||||||
'icon' => 'receipt',
|
'icon' => 'order-color',
|
||||||
'tone' => 'orders',
|
|
||||||
'url' => $orders_url,
|
'url' => $orders_url,
|
||||||
'tab' => 'orders',
|
'tab' => 'orders',
|
||||||
],
|
],
|
||||||
@@ -41,35 +42,38 @@ $services = array_values(array_filter([
|
|||||||
$settings = array_values(array_filter([
|
$settings = array_values(array_filter([
|
||||||
[
|
[
|
||||||
'label' => __('消息通知', 'aksrt'),
|
'label' => __('消息通知', 'aksrt'),
|
||||||
'icon' => 'bell',
|
'icon' => 'msg-color',
|
||||||
'tone' => 'notice',
|
|
||||||
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
||||||
'tab' => 'msg',
|
'tab' => 'msg',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('个人资料', 'aksrt'),
|
'label' => __('个人资料', 'aksrt'),
|
||||||
'icon' => 'user-edit',
|
'icon' => 'user-color',
|
||||||
'tone' => 'profile',
|
|
||||||
'url' => isset($dashboard_tabs['profile']) ? aksrt_get_user_center_tab_url('profile') : '',
|
'url' => isset($dashboard_tabs['profile']) ? aksrt_get_user_center_tab_url('profile') : '',
|
||||||
|
'tab' => 'profile',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('账户安全', 'aksrt'),
|
'label' => __('账户安全', 'aksrt'),
|
||||||
'icon' => 'shield-alt',
|
'icon' => 'security-color',
|
||||||
'tone' => 'security',
|
|
||||||
'url' => isset($dashboard_tabs['settings']) ? aksrt_get_user_center_tab_url('settings') : '',
|
'url' => isset($dashboard_tabs['settings']) ? aksrt_get_user_center_tab_url('settings') : '',
|
||||||
|
'tab' => 'settings',
|
||||||
],
|
],
|
||||||
], static fn(array $item): bool => (string) $item['url'] !== ''));
|
], static fn(array $item): bool => (string) $item['url'] !== ''));
|
||||||
|
|
||||||
$render_grid = static function (array $items): void {
|
$render_grid = static function (array $items): void {
|
||||||
?>
|
?>
|
||||||
<div class="uc-service-grid">
|
<div class="flex ac hh text-center icon-but-box user-icon-but-box">
|
||||||
<?php foreach ($items as $item) : ?>
|
<?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; ?>>
|
$tab = (string) ($item['tab'] ?? '');
|
||||||
<span class="uc-service-grid__icon">
|
$item_class = 'icon-but-' . ($tab !== '' ? $tab : 'item');
|
||||||
<i class="fa fas fa-<?php echo esc_attr((string) $item['icon']); ?>" aria-hidden="true"></i>
|
if (($active_tab ?? '') === $tab) {
|
||||||
<?php if (!empty($item['count'])) : ?><small><?php echo esc_html((string) $item['count']); ?></small><?php endif; ?>
|
$item_class .= ' is-active';
|
||||||
</span>
|
}
|
||||||
<span><?php echo esc_html((string) $item['label']); ?></span>
|
$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>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ $sidebar_level = $vip_level > 0 ? $vip_level : $expired_level;
|
|||||||
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
||||||
$sidebar_icon = $sidebar_icon_url !== ''
|
$sidebar_icon = $sidebar_icon_url !== ''
|
||||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $sidebar_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
? '<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">
|
<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__icon"><?php echo $sidebar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
@@ -57,5 +58,5 @@ $sidebar_icon = $sidebar_icon_url !== ''
|
|||||||
<strong><?php echo esc_html($title); ?></strong>
|
<strong><?php echo esc_html($title); ?></strong>
|
||||||
<small><?php echo esc_html($status); ?></small>
|
<small><?php echo esc_html($status); ?></small>
|
||||||
</span>
|
</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>
|
</a>
|
||||||
@@ -29,6 +29,8 @@ $vip_state_name = (string) ($vip_state['state'] ?? 'unopened');
|
|||||||
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
||||||
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
||||||
$desc_top = (string) ($vip_settings['desc'] ?? '');
|
$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.
|
// Membership summary: title → level → intro → expiry.
|
||||||
$vip_state = 'unopened';
|
$vip_state = 'unopened';
|
||||||
@@ -36,7 +38,9 @@ $vip_state_label = __('未开通', 'aksrt');
|
|||||||
$vip_level_number = 0;
|
$vip_level_number = 0;
|
||||||
$vip_level_name = __('未开通会员', 'aksrt');
|
$vip_level_name = __('未开通会员', 'aksrt');
|
||||||
$vip_description = $desc_top !== '' ? $desc_top : __('开通会员,尊享专属特权', 'aksrt');
|
$vip_description = $desc_top !== '' ? $desc_top : __('开通会员,尊享专属特权', 'aksrt');
|
||||||
$vip_expiry_text = __('到期时间:未开通', 'aksrt');
|
// 未开通时不留到期文案:状态标签已显示「未开通」,参考主题对从未开通
|
||||||
|
// 用户同样不输出任何到期信息(仅生效中/永久/已过期才有)。
|
||||||
|
$vip_expiry_text = '';
|
||||||
$vip_is_permanent = false;
|
$vip_is_permanent = false;
|
||||||
$btn_type = 'pay';
|
$btn_type = 'pay';
|
||||||
|
|
||||||
@@ -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>'
|
. '<span class="uc-vip-status__state">' . esc_html($vip_state_label) . '</span>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '<div class="uc-vip-status__main uc-vip-status__main--level-' . (int) $vip_level_number . '">'
|
. '<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>'
|
// 未开通(level 0)不输出图标位:名称前不放通用证书图标。
|
||||||
|
. ($vip_level_number > 0 ? '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>' : '')
|
||||||
. '<span class="uc-vip-status__level">' . esc_html($vip_level_name) . '</span>'
|
. '<span class="uc-vip-status__level">' . esc_html($vip_level_name) . '</span>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '<div class="uc-vip-status__desc">' . esc_html($vip_description) . '</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>'
|
. ($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>';
|
. '</div>';
|
||||||
|
|
||||||
// Level cards.
|
// Level cards.
|
||||||
@@ -113,6 +118,9 @@ foreach ([1, 2] as $lv) {
|
|||||||
: '';
|
: '';
|
||||||
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
||||||
$vip_attr = $lv;
|
$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_url = esc_url((string) ($levels[$lv]['img_icon'] ?? ''));
|
||||||
$lv_icon = $lv_icon_url !== ''
|
$lv_icon = $lv_icon_url !== ''
|
||||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||||
@@ -121,8 +129,10 @@ foreach ([1, 2] as $lv) {
|
|||||||
$renew_lv = (int) substr($btn_type, 6);
|
$renew_lv = (int) substr($btn_type, 6);
|
||||||
if ($lv === $renew_lv) {
|
if ($lv === $renew_lv) {
|
||||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||||
|
$card_action = 'renew';
|
||||||
} elseif ($lv > $renew_lv) {
|
} elseif ($lv > $renew_lv) {
|
||||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||||
|
$card_action = 'upgrade';
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -131,19 +141,26 @@ foreach ([1, 2] as $lv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||||
|
$card_action = 'upgrade';
|
||||||
$vip_attr = 2;
|
$vip_attr = 2;
|
||||||
} elseif ($level > 0 && $lv < $level) {
|
} elseif ($level > 0 && $lv < $level) {
|
||||||
continue;
|
continue;
|
||||||
} elseif ($level > 0 && $lv === $level) {
|
} elseif ($level > 0 && $lv === $level) {
|
||||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
$cta = sprintf(__('续费%s', 'aksrt'), $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="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="relative uc-vip-level-card__inner">';
|
||||||
$cards .= '<div class="v-iconbox mb10">';
|
$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>';
|
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . esc_html($lv_name) . '</div>';
|
||||||
if ($lv_desc !== '') {
|
if ($card_desc !== '') {
|
||||||
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($lv_desc) . '</div>';
|
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($card_desc) . '</div>';
|
||||||
}
|
}
|
||||||
$cards .= '</div>';
|
$cards .= '</div>';
|
||||||
$cards .= $equity;
|
$cards .= $equity;
|
||||||
@@ -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 -- 各字段已在生成时转义。
|
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>
|
</div>
|
||||||
@@ -79,12 +79,6 @@ class GlobalSchema {
|
|||||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
|
|
||||||
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
|
|
||||||
]],
|
|
||||||
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
||||||
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ class SchemaExtensionsPush {
|
|||||||
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||||
]],
|
]],
|
||||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
['key' => 'channel', 'title' => '收录通道', 'fields' => [
|
||||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
|
||||||
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
|
|
||||||
]],
|
]],
|
||||||
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
||||||
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
|
||||||
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
|
|||||||
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
||||||
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
||||||
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
||||||
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_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' => '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_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' => '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' => [
|
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
||||||
@@ -178,7 +178,7 @@ class SchemaPayment {
|
|||||||
'vip' => [
|
'vip' => [
|
||||||
'key' => 'vip',
|
'key' => 'vip',
|
||||||
'title' => 'VIP会员',
|
'title' => 'VIP会员',
|
||||||
'version' => '1.1.0',
|
'version' => '1.2.0',
|
||||||
'option_key' => 'aksrt_settings',
|
'option_key' => 'aksrt_settings',
|
||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||||
@@ -188,17 +188,8 @@ class SchemaPayment {
|
|||||||
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
||||||
['key' => 'nav_user_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_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' => '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_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' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
['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' => 'benefit', 'title' => '会员权益', 'fields' => [
|
||||||
['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [
|
['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_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' => '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_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' => '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_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则不限制);只对本地服务器文件有效'],
|
['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' => 'opt', 'title' => '会员购买选项', 'fields' => [
|
||||||
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
||||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => self::vip_level_tab_fields(1)],
|
||||||
['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => self::vip_level_tab_fields(2)],
|
||||||
['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' => 'renew', 'title' => '会员续费', 'fields' => [
|
['key' => 'renew', 'title' => '会员续费', 'fields' => [
|
||||||
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'],
|
['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_desc', 'label' => '续费介绍', 'type' => 'input', 'default' => '立即续费会员,畅享VIP权益', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'help' => '续费会员的一句话简介'],
|
||||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'select', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||||
['label' => '相同价格', 'value' => 'same'],
|
['label' => '相同价格', 'value' => 'same'],
|
||||||
['label' => '折扣价格', 'value' => 'discount'],
|
['label' => '折扣价格', 'value' => 'discount'],
|
||||||
['label' => '优惠立减', 'value' => 'reduce'],
|
['label' => '优惠立减', 'value' => 'reduce'],
|
||||||
]],
|
['label' => '自定义商品', 'value' => 'customize'],
|
||||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
], 'help' => '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价'],
|
||||||
['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']],
|
['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' => 'upgrade', 'title' => '会员升级', 'fields' => [
|
||||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
['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', 'default' => '升级VIP会员,享更多会员权益', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '升级会员的一句话简介'],
|
||||||
['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_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' => 'points-exchange', 'title' => '积分兑换会员', '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|积分永久", '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_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', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]],
|
||||||
|
['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 种)。
|
* PayPal / Stripe 结算货币选项(参考主题同款 26 种)。
|
||||||
*
|
*
|
||||||
@@ -576,4 +654,92 @@ class SchemaPayment {
|
|||||||
}
|
}
|
||||||
return $options;
|
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', 'default' => $level === 1 ? '黄金会员' : '钻石会员', 'dependsOn' => $dep, 'help' => '会员名称(例如“黄金会员”、“超级会员”)'],
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_desc', 'label' => '一句话简介', 'type' => 'input', 'default' => '尊享专属特权', 'dependsOn' => $dep],
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => $equity, 'rows' => 4, 'dependsOn' => $dep, 'help' => '建议使用 <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;
|
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) {
|
private function sanitize_field_value($value, array $field) {
|
||||||
if (!empty($field['captcha_provider'])) {
|
if (!empty($field['captcha_provider'])) {
|
||||||
return function_exists('aksrt_captcha_sanitize_provider_config')
|
return function_exists('aksrt_captcha_sanitize_provider_config')
|
||||||
@@ -898,6 +939,15 @@ class SchemaProvider {
|
|||||||
return $normalized !== null ? $normalized : (string) ($field['default'] ?? '');
|
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)) {
|
if (in_array($type, ['fieldset', 'accordion'], true)) {
|
||||||
return $this->sanitize_object_fields($value, (array) ($field['fields'] ?? []));
|
return $this->sanitize_object_fields($value, (array) ($field['fields'] ?? []));
|
||||||
}
|
}
|
||||||
@@ -994,6 +1044,12 @@ class SchemaProvider {
|
|||||||
if (!is_array($field) || empty($field['key'])) {
|
if (!is_array($field) || empty($field['key'])) {
|
||||||
continue;
|
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'];
|
$key = (string) $field['key'];
|
||||||
if (array_key_exists($key, $value)) {
|
if (array_key_exists($key, $value)) {
|
||||||
$clean[$key] = $this->sanitize_field_value($value[$key], $field);
|
$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 {
|
public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
if ($request->get_method() === 'GET') {
|
if ($request->get_method() === 'GET') {
|
||||||
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
||||||
|
|||||||
+41
-19
@@ -219,35 +219,57 @@ final class OAuth {
|
|||||||
'name' => $provider,
|
'name' => $provider,
|
||||||
'label' => OAuthProviderCatalog::label($provider),
|
'label' => OAuthProviderCatalog::label($provider),
|
||||||
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($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;
|
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 {
|
public function render_social_buttons(string $title = ''): string {
|
||||||
$items = $this->get_auth_providers_data();
|
$items = $this->get_auth_providers_data();
|
||||||
if ($items === []) {
|
if ($items === []) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
ob_start();
|
|
||||||
?>
|
$large = (bool) \AKSRT\Settings\Settings::get_value('oauth_button_lg', false);
|
||||||
<div class="social-login-divider">
|
$html = $title !== ''
|
||||||
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span>
|
? '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html($title) . '</p>'
|
||||||
</div>
|
: '';
|
||||||
<div class="oauth-row">
|
$html .= '<div class="social_loginbar">';
|
||||||
<?php foreach ($items as $item) : ?>
|
foreach ($items as $item) {
|
||||||
<a href="<?php echo esc_url((string) $item['url']); ?>"
|
$name = sanitize_html_class((string) ($item['name'] ?? ''));
|
||||||
class="oauth-btn oauth-btn--<?php echo esc_attr((string) $item['name']); ?>"
|
$type = str_starts_with($name, 'clogin_') ? substr($name, 7) : $name;
|
||||||
data-provider="<?php echo esc_attr((string) $item['name']); ?>"
|
$label = trim((string) ($item['label'] ?? ''));
|
||||||
aria-label="<?php echo esc_attr((string) $item['label']); ?>"
|
$url = (string) ($item['url'] ?? '');
|
||||||
title="<?php echo esc_attr((string) $item['label']); ?>">
|
if ($name === '' || $type === '' || $label === '' || $url === '') {
|
||||||
<?php echo $item['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
continue;
|
||||||
</a>
|
}
|
||||||
<?php endforeach; ?>
|
$class = 'social-login-item ' . $type . ($large ? ' button-lg' : ' toggle-radius');
|
||||||
</div>
|
if ($name === 'weixingzh') {
|
||||||
<?php
|
// Reference parity: the WeChat entry opens the QR panel instead of
|
||||||
return (string) ob_get_clean();
|
// 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 {
|
public function get_frontend_bindings_data(int $user_id): array {
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ trait VipServiceModal {
|
|||||||
'prefer_level' => $prefer_level,
|
'prefer_level' => $prefer_level,
|
||||||
'catalog' => [
|
'catalog' => [
|
||||||
'desc' => (string) ($catalog['desc'] ?? ''),
|
'desc' => (string) ($catalog['desc'] ?? ''),
|
||||||
'help_text' => (string) ($catalog['help_text'] ?? ''),
|
|
||||||
'more' => (string) ($vip['more'] ?? ''),
|
'more' => (string) ($vip['more'] ?? ''),
|
||||||
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
|
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
|
||||||
'level_packages' => $groups,
|
'level_packages' => $groups,
|
||||||
@@ -215,11 +214,13 @@ trait VipServiceModal {
|
|||||||
}
|
}
|
||||||
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
|
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
|
||||||
$points = max(0, (int) ($package['points'] ?? 0));
|
$points = max(0, (int) ($package['points'] ?? 0));
|
||||||
$days = max(0, (int) ($package['days'] ?? 0));
|
// 参考主题时长行:30<span class="px12">天</span> / 永久;time 为 Permanent 时按永久渲染。
|
||||||
// 参考主题时长行:30<span class="px12">天</span> / 永久。
|
$is_permanent = !empty($package['is_permanent']);
|
||||||
$duration = $days > 0
|
$pkg_time = max(0, (int) ($package['time'] ?? 0));
|
||||||
? (string) $days . '<span class="px12">' . esc_html__('天', 'aksrt') . '</span>'
|
$pkg_unit = ((string) ($package['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||||
: esc_html__('永久', 'aksrt');
|
$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'] ?? ('VIP' . $level));
|
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||||
|
|
||||||
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
||||||
@@ -239,7 +240,9 @@ trait VipServiceModal {
|
|||||||
|
|
||||||
$desc_text = trim((string) ($points_ex['desc'] ?? ''));
|
$desc_text = trim((string) ($points_ex['desc'] ?? ''));
|
||||||
$desc = $desc_text !== ''
|
$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">' . wp_kses_post($desc_text) . '</div>'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
||||||
|
|||||||
+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.
|
* Approximate days for catalog labels / order meta; activate prefers time+unit calendar math.
|
||||||
*/
|
*/
|
||||||
@@ -100,118 +24,59 @@ trait VipServicePackages {
|
|||||||
return $time;
|
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).
|
* Build upgrade package items (legacy vip_upgrade_product parity).
|
||||||
*
|
*
|
||||||
* upgrade.product multi-line formats:
|
* Three blocks (reference theme vip_upgrade_product accordion):
|
||||||
* - permanent VIP1: permanent|price|title[|show_price]
|
* - month_to_month_s: timed VIP1 → VIP2, priced by remaining days, expiry unchanged
|
||||||
* - fixed (alias): price|days|title
|
* - jump_s: timed VIP1 → permanent VIP2 at a fixed price
|
||||||
* - timed unit: unit|daily_price|title
|
* - permanent_*: permanent VIP1 → permanent VIP2 at a fixed price
|
||||||
* - timed jump permanent: jump|price|title[|show_price]
|
*
|
||||||
*
|
* package index: 901 permanent, 902 unit, 903 jump
|
||||||
* Rules:
|
*
|
||||||
* - permanent user: only permanent/fixed (days=0 preferred)
|
* @param array<string,mixed> $config
|
||||||
* - timed user: unit (keep expiry) + optional jump (to permanent)
|
* @return array<int, array<string,mixed>>
|
||||||
*
|
*/
|
||||||
* package index: 901 permanent/fixed, 902 unit, 903 jump
|
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, array $config): array {
|
||||||
*
|
|
||||||
* @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 [];
|
|
||||||
}
|
|
||||||
$items = [];
|
$items = [];
|
||||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
$now = time();
|
||||||
$has_permanent = false;
|
|
||||||
$has_unit = false;
|
|
||||||
$has_jump = false;
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
// 月费会员升级月费会员:按剩余天数计价,会员有效期不变(仅提升等级)。
|
||||||
$line = trim((string) $line);
|
if (!$is_permanent && $expires > $now && !empty($config['month_to_month_s'])) {
|
||||||
if ($line === '') {
|
$daily_minor = $this->parse_vip_money_minor((string) ($config['unit_price'] ?? ''));
|
||||||
continue;
|
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 && !empty($config['jump_s'])) {
|
||||||
if ($is_permanent || $expires <= time() || $has_unit) {
|
$price_minor = $this->parse_vip_money_minor((string) ($config['jump_price'] ?? ''));
|
||||||
continue;
|
if ($price_minor > 0) {
|
||||||
}
|
|
||||||
$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;
|
|
||||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
$show_minor = $this->parse_vip_money_minor((string) ($config['jump_show_price'] ?? ''));
|
||||||
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 = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'package_id' => $this->encode_package_id(2, 903),
|
'package_id' => $this->encode_package_id(2, 903),
|
||||||
@@ -221,9 +86,10 @@ trait VipServicePackages {
|
|||||||
'price' => $price,
|
'price' => $price,
|
||||||
'base_price' => $price,
|
'base_price' => $price,
|
||||||
'show_price' => $show,
|
'show_price' => $show,
|
||||||
'discount_tag' => __('永久升级', 'aksrt'),
|
'discount_tag' => (string) ($config['jump_tag'] ?? ''),
|
||||||
|
'tag_class' => (string) ($config['jump_tag_class'] ?? ''),
|
||||||
'days' => 0,
|
'days' => 0,
|
||||||
'title' => $title,
|
'title' => __('升级为永久会员', 'aksrt'),
|
||||||
'price_label' => $this->format_cash_label($price),
|
'price_label' => $this->format_cash_label($price),
|
||||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||||
'days_label' => __('永久', 'aksrt'),
|
'days_label' => __('永久', 'aksrt'),
|
||||||
@@ -231,25 +97,15 @@ trait VipServicePackages {
|
|||||||
'is_upgrade_product' => true,
|
'is_upgrade_product' => true,
|
||||||
'upgrade_mode' => 'jump',
|
'upgrade_mode' => 'jump',
|
||||||
];
|
];
|
||||||
$has_jump = true;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// permanent|price|title[|show_price]
|
// 永久会员升级永久会员。
|
||||||
if ($mode === 'permanent') {
|
if ($is_permanent) {
|
||||||
if ($has_permanent) {
|
$price_minor = $this->parse_vip_money_minor((string) ($config['permanent_price'] ?? ''));
|
||||||
continue;
|
if ($price_minor > 0) {
|
||||||
}
|
|
||||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
|
||||||
if ($price_minor <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
$show_minor = $this->parse_vip_money_minor((string) ($config['permanent_show_price'] ?? ''));
|
||||||
if ($title === '') {
|
|
||||||
$title = __('永久会员升级', 'aksrt');
|
|
||||||
}
|
|
||||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
|
||||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'package_id' => $this->encode_package_id(2, 901),
|
'package_id' => $this->encode_package_id(2, 901),
|
||||||
@@ -259,9 +115,10 @@ trait VipServicePackages {
|
|||||||
'price' => $price,
|
'price' => $price,
|
||||||
'base_price' => $price,
|
'base_price' => $price,
|
||||||
'show_price' => $show,
|
'show_price' => $show,
|
||||||
'discount_tag' => __('升级专享', 'aksrt'),
|
'discount_tag' => (string) ($config['permanent_tag'] ?? ''),
|
||||||
|
'tag_class' => (string) ($config['permanent_tag_class'] ?? ''),
|
||||||
'days' => 0,
|
'days' => 0,
|
||||||
'title' => $title,
|
'title' => __('永久会员升级', 'aksrt'),
|
||||||
'price_label' => $this->format_cash_label($price),
|
'price_label' => $this->format_cash_label($price),
|
||||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||||
'days_label' => __('永久', 'aksrt'),
|
'days_label' => __('永久', 'aksrt'),
|
||||||
@@ -269,49 +126,7 @@ trait VipServicePackages {
|
|||||||
'is_upgrade_product' => true,
|
'is_upgrade_product' => true,
|
||||||
'upgrade_mode' => 'permanent',
|
'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;
|
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<string,mixed> $vip
|
||||||
* @param array<int,mixed> $levels
|
* @param array<int,mixed> $levels
|
||||||
@@ -328,47 +146,49 @@ trait VipServicePackages {
|
|||||||
*/
|
*/
|
||||||
public function get_points_exchange_packages(array $vip, array $levels): array {
|
public function get_points_exchange_packages(array $vip, array $levels): array {
|
||||||
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
|
$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');
|
$currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||||
$out = [];
|
$out = [];
|
||||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
|
||||||
$i = 0;
|
$i = 0;
|
||||||
foreach ($lines as $line) {
|
foreach ($rows as $row) {
|
||||||
$line = trim((string) $line);
|
if (!is_array($row)) {
|
||||||
if ($line === '') {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$parts = array_map('trim', explode('|', $line));
|
$lv = (int) ($row['level'] ?? 0);
|
||||||
if (count($parts) < 3) {
|
$pts = max(0, (int) ($row['points'] ?? 0));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$lv = (int) ($parts[0] ?? 0);
|
|
||||||
$pts = (int) ($parts[1] ?? 0);
|
|
||||||
$days = (int) ($parts[2] ?? 0);
|
|
||||||
$title = sanitize_text_field((string) ($parts[3] ?? ''));
|
|
||||||
if ($lv < 1 || $lv > 2 || $pts <= 0) {
|
if ($lv < 1 || $lv > 2 || $pts <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (empty($levels[$lv]['enabled'])) {
|
if (empty($levels[$lv]['enabled'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$i++;
|
$is_permanent = !empty($row['is_permanent']);
|
||||||
if ($title === '') {
|
$time = max(0, (int) ($row['time'] ?? 0));
|
||||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||||
$title = $days > 0
|
$days = $is_permanent ? 0 : max(0, (int) ($row['days'] ?? 0));
|
||||||
? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days)
|
if (!$is_permanent && $time <= 0) {
|
||||||
: sprintf(__('%s · 永久', 'aksrt'), $lv_name);
|
continue;
|
||||||
}
|
}
|
||||||
|
$i++;
|
||||||
|
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||||
$out[] = [
|
$out[] = [
|
||||||
'package_id' => 5000 + $i,
|
'package_id' => 5000 + $i,
|
||||||
'level' => $lv,
|
'level' => $lv,
|
||||||
'index' => $i,
|
'index' => $i,
|
||||||
'points' => $pts,
|
'points' => $pts,
|
||||||
'days' => max(0, $days),
|
'days' => $days,
|
||||||
'title' => $title,
|
'time' => $time,
|
||||||
'price_label' => $pts . ' ' . $currency,
|
'unit' => $unit,
|
||||||
'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'),
|
'is_permanent' => $is_permanent,
|
||||||
'cta_label' => __('立即兑换', 'aksrt'),
|
'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;
|
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.
|
* 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> $renew
|
||||||
* @param array<string,mixed> $upgrade
|
* @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(
|
private function apply_action_price(
|
||||||
string $base_price,
|
array $pkg,
|
||||||
string $action,
|
string $action,
|
||||||
array $renew,
|
array $renew,
|
||||||
array $upgrade,
|
array $upgrade,
|
||||||
@@ -420,6 +217,7 @@ trait VipServicePackages {
|
|||||||
bool $is_permanent,
|
bool $is_permanent,
|
||||||
int $days
|
int $days
|
||||||
): ?array {
|
): ?array {
|
||||||
|
$base_price = (string) ($pkg['price'] ?? '');
|
||||||
$base_minor = $this->parse_vip_money_minor($base_price);
|
$base_minor = $this->parse_vip_money_minor($base_price);
|
||||||
if ($base_minor <= 0) {
|
if ($base_minor <= 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -434,32 +232,31 @@ trait VipServicePackages {
|
|||||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($base_minor),
|
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($base_minor),
|
||||||
'show_price' => '0.00',
|
'show_price' => '0.00',
|
||||||
'tag' => '',
|
'tag' => '',
|
||||||
|
'tag_class' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
|
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
|
||||||
|
// discount/reduce 的标签由优惠幅度计算得出,若实际没有优惠则属于虚假折扣。
|
||||||
|
$computed_tag = in_array($type, ['discount', 'reduce'], true);
|
||||||
$price_minor = $base_minor;
|
$price_minor = $base_minor;
|
||||||
$show_minor = $base_minor;
|
$show_minor = $base_minor;
|
||||||
$tag = '';
|
$tag = '';
|
||||||
|
$tag_class = '';
|
||||||
|
|
||||||
if ($type === 'discount') {
|
if ($type === 'discount') {
|
||||||
$discount = (int) ($renew['discount'] ?? 8);
|
$discount = (float) ($renew['discount'] ?? 8);
|
||||||
if ($discount <= 0) {
|
if ($discount <= 0) {
|
||||||
$discount = 8;
|
$discount = 8;
|
||||||
}
|
}
|
||||||
if ($discount <= 10) {
|
// 折值:8 = 八折(参考主题 slider 0.1–9.9 语义)。
|
||||||
$percent = $discount * 10;
|
$discount = max(0.1, min(9.9, $discount));
|
||||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $discount * 10);
|
||||||
$tag = sprintf(
|
$tag = sprintf(
|
||||||
/* translators: %s: discount like 8 */
|
/* translators: %s: discount like 8 */
|
||||||
__('%s折续费', 'aksrt'),
|
__('%s折优惠', 'aksrt'),
|
||||||
(string) $discount
|
rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.') ?: '0'
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
$percent = min(100, $discount);
|
|
||||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
|
||||||
$tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount);
|
|
||||||
}
|
|
||||||
} elseif ($type === 'reduce') {
|
} elseif ($type === 'reduce') {
|
||||||
$reduce_minor = $this->parse_vip_money_minor($renew['reduce'] ?? '20');
|
$reduce_minor = $this->parse_vip_money_minor($renew['reduce'] ?? '20');
|
||||||
$price_minor = max(1, $base_minor - $reduce_minor);
|
$price_minor = max(1, $base_minor - $reduce_minor);
|
||||||
@@ -468,6 +265,11 @@ trait VipServicePackages {
|
|||||||
__('续费立减%s', 'aksrt'),
|
__('续费立减%s', 'aksrt'),
|
||||||
$this->format_cash_label(\AKSRT\Ledger\AssetAmount::decimal_from_minor($reduce_minor))
|
$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 {
|
} else {
|
||||||
$show_minor = 0;
|
$show_minor = 0;
|
||||||
$tag = __('续费同价', 'aksrt');
|
$tag = __('续费同价', 'aksrt');
|
||||||
@@ -477,14 +279,18 @@ trait VipServicePackages {
|
|||||||
$price_minor = 1;
|
$price_minor = 1;
|
||||||
}
|
}
|
||||||
if ($show_minor > 0 && $price_minor >= $show_minor) {
|
if ($show_minor > 0 && $price_minor >= $show_minor) {
|
||||||
|
// 参考主题契约:原价仅在低于执行价时展示。
|
||||||
$show_minor = 0;
|
$show_minor = 0;
|
||||||
$tag = __('续费', 'aksrt');
|
if ($computed_tag) {
|
||||||
|
$tag = __('续费', 'aksrt');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor > $price_minor ? $show_minor : 0),
|
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor > $price_minor ? $show_minor : 0),
|
||||||
'tag' => $tag,
|
'tag' => $tag,
|
||||||
|
'tag_class' => $tag_class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,20 +139,30 @@ trait VipServicePurchase {
|
|||||||
|
|
||||||
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||||
$items = [];
|
$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) {
|
foreach ($packages as $idx => $pkg) {
|
||||||
|
if (!is_array($pkg)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$base_price = (string) $pkg['price'];
|
$base_price = (string) $pkg['price'];
|
||||||
$days = (int) $pkg['days'];
|
$days = (int) $pkg['days'];
|
||||||
$action = 'pay';
|
$action = $level_action;
|
||||||
if ($level > 0 && $lv === $level) {
|
|
||||||
$action = 'renew';
|
|
||||||
} elseif ($level > 0 && $lv > $level) {
|
|
||||||
$action = 'upgrade';
|
|
||||||
}
|
|
||||||
|
|
||||||
$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) {
|
if ($priced === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -167,6 +177,10 @@ trait VipServicePurchase {
|
|||||||
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
|
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
|
||||||
$discount_tag = (string) $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');
|
$days_label = __('永久', 'aksrt');
|
||||||
if ((int) $pkg['time'] > 0) {
|
if ((int) $pkg['time'] > 0) {
|
||||||
@@ -184,10 +198,10 @@ trait VipServicePurchase {
|
|||||||
'base_price' => $base_price,
|
'base_price' => $base_price,
|
||||||
'show_price' => $show_price,
|
'show_price' => $show_price,
|
||||||
'discount_tag' => $discount_tag,
|
'discount_tag' => $discount_tag,
|
||||||
|
'tag_class' => $tag_class,
|
||||||
'days' => $days,
|
'days' => $days,
|
||||||
'time' => (int) ($pkg['time'] ?? 0),
|
'time' => (int) ($pkg['time'] ?? 0),
|
||||||
'unit' => (string) ($pkg['unit'] ?? 'day'),
|
'unit' => (string) ($pkg['unit'] ?? 'day'),
|
||||||
'title' => (string) $pkg['title'],
|
|
||||||
'price_label' => $this->format_cash_label((string) $priced['price']),
|
'price_label' => $this->format_cash_label((string) $priced['price']),
|
||||||
'show_price_label' => $show_minor > 0
|
'show_price_label' => $show_minor > 0
|
||||||
? $this->format_cash_label($show_price)
|
? $this->format_cash_label($show_price)
|
||||||
@@ -201,7 +215,7 @@ trait VipServicePurchase {
|
|||||||
|
|
||||||
// Dedicated upgrade product: fixed or remaining-days unit price.
|
// Dedicated upgrade product: fixed or remaining-days unit price.
|
||||||
if ($level === 1 && $lv === 2 && $upgrade_enabled) {
|
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) {
|
foreach ($upgrade_items as $ui) {
|
||||||
$items[] = $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;
|
$balances = \AKSRT\Ledger\LedgerService::get_instance()->points_and_balance($user_id);
|
||||||
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
|
|
||||||
$methods = $this->available_payment_methods();
|
$methods = $this->available_payment_methods();
|
||||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||||
|
|
||||||
$catalog = [
|
$catalog = [
|
||||||
'enabled' => !empty($vip['enabled']),
|
'enabled' => !empty($vip['enabled']),
|
||||||
'desc' => (string) ($vip['desc'] ?? ''),
|
'desc' => (string) ($vip['desc'] ?? ''),
|
||||||
'help_text' => (string) ($vip['help_text'] ?? ''),
|
|
||||||
'user_level' => $level,
|
'user_level' => $level,
|
||||||
'user_expires' => $expires,
|
'user_expires' => $expires,
|
||||||
'user_is_permanent' => $is_permanent,
|
'user_is_permanent' => $is_permanent,
|
||||||
'user_expires_label' => (string) ($member['expires_label'] ?? ''),
|
'user_expires_label' => (string) ($member['expires_label'] ?? ''),
|
||||||
'user_points' => $points_balance,
|
'user_points' => $balances['points'],
|
||||||
'wallet_balance' => $wallet_balance,
|
'wallet_balance' => $balances['balance'],
|
||||||
'currency_name' => $currency_name,
|
'currency_name' => $currency_name,
|
||||||
'renew_enabled' => $renew_enabled,
|
'renew_enabled' => $renew_enabled,
|
||||||
'upgrade_enabled' => $upgrade_enabled,
|
'upgrade_enabled' => $upgrade_enabled,
|
||||||
@@ -443,11 +455,12 @@ trait VipServicePurchase {
|
|||||||
'note' => is_string($note) ? $note : '',
|
'note' => is_string($note) ? $note : '',
|
||||||
'expires_at' => $expires_at,
|
'expires_at' => $expires_at,
|
||||||
'subject' => sprintf(
|
'subject' => sprintf(
|
||||||
/* translators: 1: action 2: vip name 3: package title */
|
/* translators: 1: action 2: vip name 3: price label 4: duration label */
|
||||||
__('%1$s%2$s · %3$s', 'aksrt'),
|
__('%1$s%2$s · %3$s / %4$s', 'aksrt'),
|
||||||
$action_label,
|
$action_label,
|
||||||
$level_name,
|
$level_name,
|
||||||
(string) ($pkg['title'] ?? '')
|
(string) ($pkg['price_label'] ?? ''),
|
||||||
|
(string) ($pkg['days_label'] ?? '')
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -759,6 +772,11 @@ trait VipServicePurchase {
|
|||||||
$cost = (int) ($match['points'] ?? 0);
|
$cost = (int) ($match['points'] ?? 0);
|
||||||
$level = (int) ($match['level'] ?? 1);
|
$level = (int) ($match['level'] ?? 1);
|
||||||
$days = (int) ($match['days'] ?? 0);
|
$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) {
|
if ($cost <= 0) {
|
||||||
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
|
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
|
||||||
}
|
}
|
||||||
@@ -781,6 +799,8 @@ trait VipServicePurchase {
|
|||||||
'vip_pay_type' => 'points_exchange',
|
'vip_pay_type' => 'points_exchange',
|
||||||
'level' => $level,
|
'level' => $level,
|
||||||
'days' => $days,
|
'days' => $days,
|
||||||
|
'time' => $pkg_time,
|
||||||
|
'unit' => $pkg_unit,
|
||||||
'points' => $cost,
|
'points' => $cost,
|
||||||
'package_id' => $package_id,
|
'package_id' => $package_id,
|
||||||
'package_title' => (string) ($match['title'] ?? ''),
|
'package_title' => (string) ($match['title'] ?? ''),
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ trait ScriptsPart1b {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 顶部导航用户卡片(legacy header-userbox 模块样式)。
|
// 顶部导航用户卡片(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')) {
|
if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) {
|
||||||
$auth_css = get_template_directory() . '/assets/css/routes/sign.css';
|
$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 {
|
public function track_visit(): void {
|
||||||
// 排除管理员、爬虫、AJAX请求
|
// 排除管理员、爬虫、AJAX请求
|
||||||
@@ -41,6 +44,12 @@ class StatsTracker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static $scheduled = false;
|
||||||
|
if ($scheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$scheduled = true;
|
||||||
|
|
||||||
$today = current_time('Y-m-d');
|
$today = current_time('Y-m-d');
|
||||||
$option_key = 'aksrt_daily_stats_' . $today;
|
$option_key = 'aksrt_daily_stats_' . $today;
|
||||||
|
|
||||||
@@ -48,6 +57,19 @@ class StatsTracker {
|
|||||||
$visitor_cookie = 'aksrt_visitor_' . $today;
|
$visitor_cookie = 'aksrt_visitor_' . $today;
|
||||||
$is_unique = empty($_COOKIE[$visitor_cookie]);
|
$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]);
|
$stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]);
|
||||||
|
|
||||||
@@ -59,7 +81,6 @@ class StatsTracker {
|
|||||||
$stats['pageviews']++;
|
$stats['pageviews']++;
|
||||||
if ($is_unique) {
|
if ($is_unique) {
|
||||||
$stats['visitors']++;
|
$stats['visitors']++;
|
||||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update_option($option_key, $stats, false);
|
update_option($option_key, $stats, false);
|
||||||
|
|||||||
+279
-63
@@ -9,6 +9,14 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
class SearchPush {
|
class SearchPush {
|
||||||
|
|
||||||
|
/** 推送结果 meta 键(编辑页面板与去重逻辑共用)。 */
|
||||||
|
public const META_KEY = '_aksrt_baidu_push_back';
|
||||||
|
|
||||||
|
/** 「重新提交」nonce 与复选框字段名(与 SearchPushMetaBox 共用)。 */
|
||||||
|
public const NONCE_ACTION = 'aksrt_push_meta_save';
|
||||||
|
public const NONCE_FIELD = 'aksrt_push_meta_nonce';
|
||||||
|
public const RESUBMIT_FIELD = 'aksrt_push_resubmit';
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
public static function get_instance(): self {
|
public static function get_instance(): self {
|
||||||
@@ -17,16 +25,20 @@ class SearchPush {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绑定运行时钩子:发布/更新主动推送 + 前台自动推送 JS。
|
* 绑定运行时钩子:发布/更新主动推送 + 分类法保存推送 + 前台自动推送 JS。
|
||||||
*/
|
*/
|
||||||
public function init(): void {
|
public function init(): void {
|
||||||
add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3);
|
add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3);
|
||||||
|
add_action('saved_term', [$this, 'on_saved_term'], 10, 3);
|
||||||
add_action('wp_footer', [$this, 'output_auto_push_script'], 99);
|
add_action('wp_footer', [$this, 'output_auto_push_script'], 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文章发布/更新时主动推送链接到百度。
|
* 文章发布/更新时主动推送链接到百度。
|
||||||
*
|
*
|
||||||
|
* 与参考主题一致:推送结果写入 meta,成功后不再重复提交;
|
||||||
|
* 编辑页勾选「重新提交」可清空记录并强制再次推送。
|
||||||
|
*
|
||||||
* @param string $new_status 新状态。
|
* @param string $new_status 新状态。
|
||||||
* @param string $old_status 旧状态。
|
* @param string $old_status 旧状态。
|
||||||
* @param \WP_Post $post 文章对象。
|
* @param \WP_Post $post 文章对象。
|
||||||
@@ -42,27 +54,230 @@ class SearchPush {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 仅推送公开可索引的内容类型。
|
// 仅推送公开可索引的内容类型。
|
||||||
$allowed = function_exists('aksrt_searchable_post_types')
|
if (!in_array($post->post_type, self::pushable_post_types(), true)) {
|
||||||
? aksrt_searchable_post_types()
|
|
||||||
: ['post'];
|
|
||||||
if (!in_array($post->post_type, $allowed, true)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = $this->get_config();
|
$config = $this->get_config();
|
||||||
|
if (!$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'])) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$is_new = $old_status !== 'publish';
|
$outcome = $this->push_url($url);
|
||||||
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
self::save_push_record($object_id, $this->build_record($outcome, $config), $is_term);
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装推送记录:通道结果 + 更新时间(结构与参考主题 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'])) {
|
if (!empty($config['baidu_push_fast'])) {
|
||||||
return;
|
$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' => []];
|
return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
|
// 收录通道与百度接口 type 参数对应:普通收录不带参数,快速收录为 daily。
|
||||||
|
$channels = [];
|
||||||
|
if (!empty($config['baidu_push_normal'])) {
|
||||||
|
$channels['normal'] = '';
|
||||||
|
}
|
||||||
|
if (!empty($config['baidu_push_fast'])) {
|
||||||
|
$channels['daily'] = '&type=daily';
|
||||||
|
}
|
||||||
|
if (!$channels) {
|
||||||
|
return ['success' => false, 'message' => __('未启用普通收录或快速收录通道', 'aksrt'), 'result' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = false;
|
||||||
|
$results = [];
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
foreach ($channels as $channel => $suffix) {
|
||||||
|
$outcome = $this->submit($url, $token, $suffix, $config);
|
||||||
|
$results[$channel] = $outcome['result'];
|
||||||
|
if ($outcome['success']) {
|
||||||
|
$success = true;
|
||||||
|
} else {
|
||||||
|
$messages[] = $outcome['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $results];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => implode('; ', $messages), 'result' => $results];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向百度链接提交接口发送单条 URL。
|
||||||
|
*
|
||||||
|
* @param string $url 待推送的完整 URL。
|
||||||
|
* @param string $token 百度准入密钥。
|
||||||
|
* @param string $suffix 接口附加参数(快速收录为 &type=daily)。
|
||||||
|
* @param array $config 推送配置。
|
||||||
|
* @return array{success: bool, message: string, result: array}
|
||||||
|
*/
|
||||||
|
private function submit(string $url, string $token, string $suffix, array $config): array {
|
||||||
|
$api_url = 'http://data.zz.baidu.com/urls?site=' . rawurlencode(home_url('/')) . '&token=' . $token . $suffix;
|
||||||
|
|
||||||
$response = wp_remote_post($api_url, [
|
$response = wp_remote_post($api_url, [
|
||||||
'timeout' => 10,
|
'timeout' => 10,
|
||||||
@@ -105,7 +363,7 @@ class SearchPush {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
if (is_wp_error($response)) {
|
||||||
$this->log('error', $url, $response->get_error_message());
|
$this->log($config, 'error', $url, $response->get_error_message());
|
||||||
return ['success' => false, 'message' => $response->get_error_message(), 'result' => []];
|
return ['success' => false, 'message' => $response->get_error_message(), 'result' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,59 +371,15 @@ class SearchPush {
|
|||||||
$status = wp_remote_retrieve_response_code($response);
|
$status = wp_remote_retrieve_response_code($response);
|
||||||
|
|
||||||
if ($status === 200 && isset($body['success'])) {
|
if ($status === 200 && isset($body['success'])) {
|
||||||
$this->log('success', $url, wp_json_encode($body));
|
$this->log($config, 'success', $url, wp_json_encode($body));
|
||||||
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body];
|
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body];
|
||||||
}
|
}
|
||||||
|
|
||||||
$err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status));
|
$err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status));
|
||||||
$this->log('error', $url, $err_msg);
|
$this->log($config, 'error', $url, $err_msg);
|
||||||
return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []];
|
return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量推送 URL
|
|
||||||
*
|
|
||||||
* @param string[] $urls
|
|
||||||
* @return array{success: int, failed: int, results: array}
|
|
||||||
*/
|
|
||||||
public function push_batch(array $urls): array {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$batch_size = max(1, min(500, (int) ($config['baidu_push_batch_size'] ?? 50)));
|
|
||||||
$success = 0;
|
|
||||||
$failed = 0;
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
$chunks = array_chunk($urls, $batch_size);
|
|
||||||
foreach ($chunks as $chunk) {
|
|
||||||
$token = (string) ($config['baidu_push_token'] ?? '');
|
|
||||||
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
|
|
||||||
$body_str = implode("\n", $chunk);
|
|
||||||
|
|
||||||
$response = wp_remote_post($api_url, [
|
|
||||||
'timeout' => 15,
|
|
||||||
'headers' => ['Content-Type' => 'text/plain'],
|
|
||||||
'body' => $body_str,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
$failed += count($chunk);
|
|
||||||
$results[] = ['error' => $response->get_error_message()];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
|
||||||
if (isset($body['success'])) {
|
|
||||||
$success += (int) ($body['success'] ?? 0);
|
|
||||||
$failed += (int) ($body['not_same_site'] ?? 0) + (int) ($body['not_valid'] ?? 0);
|
|
||||||
} else {
|
|
||||||
$failed += count($chunk);
|
|
||||||
}
|
|
||||||
$results[] = $body ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => $success, 'failed' => $failed, 'results' => $results];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取自动推送 JS 代码
|
* 获取自动推送 JS 代码
|
||||||
*/
|
*/
|
||||||
@@ -175,7 +389,6 @@ class SearchPush {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$site_url = home_url('/');
|
|
||||||
return <<<SCRIPT
|
return <<<SCRIPT
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
@@ -211,13 +424,15 @@ SCRIPT;
|
|||||||
'provider' => 'baidu',
|
'provider' => 'baidu',
|
||||||
'configured' => $enabled && $token !== '',
|
'configured' => $enabled && $token !== '',
|
||||||
'available' => $enabled && $token !== '',
|
'available' => $enabled && $token !== '',
|
||||||
'verified' => !empty($config['baidu_push_verified']),
|
// 百度链接提交接口无单独校验握手,Token 配置完成即可推送。
|
||||||
|
'verified' => false,
|
||||||
'transport' => 'http_api',
|
'transport' => 'http_api',
|
||||||
'missing_fields' => $missing,
|
'missing_fields' => $missing,
|
||||||
'auto_push' => !empty($config['baidu_push_auto']),
|
'auto_push' => !empty($config['baidu_push_auto']),
|
||||||
|
'normal_push' => !empty($config['baidu_push_normal']),
|
||||||
|
'fast_push' => !empty($config['baidu_push_fast']),
|
||||||
'on_publish' => !empty($config['baidu_push_on_publish']),
|
'on_publish' => !empty($config['baidu_push_on_publish']),
|
||||||
'on_update' => !empty($config['baidu_push_on_update']),
|
'on_update' => !empty($config['baidu_push_on_update']),
|
||||||
'batch_size' => max(1, (int) ($config['baidu_push_batch_size'] ?? 50)),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +443,8 @@ SCRIPT;
|
|||||||
return ['baidu_push_enabled' => false];
|
return ['baidu_push_enabled' => false];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function log(string $level, string $url, string $message): void {
|
private function log(array $config, string $level, string $url, string $message): void {
|
||||||
|
if (empty($config['baidu_push_log_enabled'])) return;
|
||||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||||
error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message));
|
error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Extensions;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度推送编辑页面板(对齐参考主题「百度资源提交」Metabox)。
|
||||||
|
*
|
||||||
|
* 文章:侧栏 Metabox;分类法:编辑 / 新增表单行。
|
||||||
|
* 展示推送结果与更新时间,并提供「重新提交」勾选——
|
||||||
|
* 勾选后保存会清空去重记录并强制再次推送。
|
||||||
|
*/
|
||||||
|
class SearchPushMetaBox {
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void {
|
||||||
|
add_action('add_meta_boxes', [$this, 'register_post_meta_box']);
|
||||||
|
// 分类法可能由插件延后注册,面板在 admin_init 统一挂载。
|
||||||
|
add_action('admin_init', [$this, 'register_term_panels']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为可推送文章类型注册侧栏 Metabox(推送功能就绪后才展示)。
|
||||||
|
*/
|
||||||
|
public function register_post_meta_box(): void {
|
||||||
|
if (!$this->is_ready()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SearchPush::pushable_post_types() as $post_type) {
|
||||||
|
add_meta_box(
|
||||||
|
'aksrt_baidu_push',
|
||||||
|
__('百度资源提交', 'aksrt'),
|
||||||
|
[$this, 'render_post_panel'],
|
||||||
|
$post_type,
|
||||||
|
'side',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为可推送分类法注册编辑 / 新增表单面板。
|
||||||
|
*/
|
||||||
|
public function register_term_panels(): void {
|
||||||
|
if (!$this->is_ready()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SearchPush::pushable_taxonomies() as $taxonomy) {
|
||||||
|
add_action("{$taxonomy}_edit_form_fields", [$this, 'render_term_panel'], 20);
|
||||||
|
add_action("{$taxonomy}_add_form_fields", [$this, 'render_term_panel'], 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染文章 Metabox。
|
||||||
|
*
|
||||||
|
* @param mixed $post WP_Post 或 null。
|
||||||
|
*/
|
||||||
|
public function render_post_panel($post): void {
|
||||||
|
if (!$post instanceof \WP_Post) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->render_panel((int) $post->ID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染分类法表单行(新增表单无术语记录,仅展示提示)。
|
||||||
|
*
|
||||||
|
* @param mixed $term WP_Term、分类名字符串或 null。
|
||||||
|
*/
|
||||||
|
public function render_term_panel($term = null): void {
|
||||||
|
$term_id = 0;
|
||||||
|
if ($term instanceof \WP_Term) {
|
||||||
|
$term_id = (int) $term->term_id;
|
||||||
|
} elseif (isset($_GET['tag_ID'])) {
|
||||||
|
$term_id = (int) $_GET['tag_ID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增表单无表格结构,仅编辑表单以表单行包裹。
|
||||||
|
$this->render_panel($term_id, true, $term_id > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function is_ready(): bool {
|
||||||
|
return class_exists(SearchPush::class) && SearchPush::get_instance()->is_ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染推送结果面板。
|
||||||
|
*
|
||||||
|
* @param int $object_id 文章 / 术语 ID(0 表示新增表单)。
|
||||||
|
* @param bool $is_term 是否为术语。
|
||||||
|
* @param bool $as_row 是否以分类法表单行包裹。
|
||||||
|
*/
|
||||||
|
private function render_panel(int $object_id, bool $is_term, bool $as_row = false): void {
|
||||||
|
ob_start();
|
||||||
|
wp_nonce_field(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD);
|
||||||
|
|
||||||
|
$record = $object_id > 0 ? SearchPush::get_push_record($object_id, $is_term) : [];
|
||||||
|
$lines = $this->result_lines($record);
|
||||||
|
?>
|
||||||
|
<div class="aksrt-push-meta-box">
|
||||||
|
<?php if ($lines) : ?>
|
||||||
|
<p class="aksrt-push-meta-box__label"><strong><?php esc_html_e('提交结果:', 'aksrt'); ?></strong></p>
|
||||||
|
<ul class="aksrt-push-meta-box__results">
|
||||||
|
<?php foreach ($lines as $line) : ?>
|
||||||
|
<li><strong><?php echo esc_html($line[0]); ?></strong> <span><?php echo esc_html($line[1]); ?></span></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php else : ?>
|
||||||
|
<p class="description"><?php esc_html_e('发布、更新后刷新页面后可查看提交结果', 'aksrt'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($record['update_time'])) : ?>
|
||||||
|
<p class="aksrt-push-meta-box__resubmit">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="<?php echo esc_attr(SearchPush::RESUBMIT_FIELD); ?>" value="1" />
|
||||||
|
<?php esc_html_e('重新提交', 'aksrt'); ?>
|
||||||
|
</label>
|
||||||
|
<span class="description"><?php esc_html_e('勾选后保存将清空提交记录并再次推送', 'aksrt'); ?></span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$body = (string) ob_get_clean();
|
||||||
|
|
||||||
|
if ($as_row) {
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
|
||||||
|
echo '<tr class="form-field"><th scope="row">' . esc_html(__('百度资源提交', 'aksrt')) . '</th><td>' . $body . '</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
|
||||||
|
echo $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总推送结果行(展示项与参考主题对齐)。
|
||||||
|
*
|
||||||
|
* @param array $record 推送记录。
|
||||||
|
* @return array<int,array{0:string,1:string}>
|
||||||
|
*/
|
||||||
|
private function result_lines(array $record): array {
|
||||||
|
$lines = [];
|
||||||
|
|
||||||
|
if (!empty($record['normal_push'])) {
|
||||||
|
$lines[] = [__('普通收录:成功', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
|
||||||
|
} elseif (array_key_exists('normal_push', $record)) {
|
||||||
|
$lines[] = [__('普通收录:失败', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($record['daily_push'])) {
|
||||||
|
$lines[] = [__('快速收录:成功', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
|
||||||
|
} elseif (array_key_exists('daily_push', $record)) {
|
||||||
|
$lines[] = [__('快速收录:失败', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($record['update_time'])) {
|
||||||
|
$lines[] = [__('更新时间:', 'aksrt'), (string) $record['update_time']];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化接口返回结果。
|
||||||
|
*
|
||||||
|
* @param mixed $result 百度接口返回的解码数据。
|
||||||
|
*/
|
||||||
|
private function format_result($result): string {
|
||||||
|
if (is_array($result) || is_object($result)) {
|
||||||
|
return (string) wp_json_encode($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,46 @@ final class LedgerService {
|
|||||||
return AssetAmount::from_minor(AssetType::BALANCE, $this->balance($user_id, AssetType::BALANCE))->decimal();
|
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 {
|
public function format_balance(int $minor): string {
|
||||||
return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted();
|
return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ trait DisplayPageGroups {
|
|||||||
'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')),
|
'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')),
|
||||||
'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_prompt'] ?? '')),
|
'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_prompt'] ?? '')),
|
||||||
'ai_seo_description_prompt' => trim((string) ($s['ai_seo_description_prompt'] ?? '')),
|
'ai_seo_description_prompt' => trim((string) ($s['ai_seo_description_prompt'] ?? '')),
|
||||||
'seo_baidu_push' => !empty($s['seo_baidu_push']) || !empty($s['baidu_push_enabled']),
|
|
||||||
'seo_baidu_normal' => !empty($s['seo_baidu_normal']),
|
|
||||||
'seo_baidu_fast' => !empty($s['seo_baidu_fast']),
|
|
||||||
'seo_baidu_token' => (string) (($s['seo_baidu_token'] ?? '') ?: ($s['baidu_push_token'] ?? '')),
|
|
||||||
'seo_image_optimize' => !isset($s['seo_image_optimize']) || !empty($s['seo_image_optimize']),
|
'seo_image_optimize' => !isset($s['seo_image_optimize']) || !empty($s['seo_image_optimize']),
|
||||||
'seo_image_default' => esc_url_raw((string) ($s['seo_image_default'] ?? '')),
|
'seo_image_default' => esc_url_raw((string) ($s['seo_image_default'] ?? '')),
|
||||||
'seo_image_always_default'=> !empty($s['seo_image_always_default']),
|
'seo_image_always_default'=> !empty($s['seo_image_always_default']),
|
||||||
|
|||||||
@@ -128,29 +128,15 @@ class ExtensionRuntime {
|
|||||||
public static function get_search_push_settings(): array {
|
public static function get_search_push_settings(): array {
|
||||||
$settings = Settings::get_all();
|
$settings = Settings::get_all();
|
||||||
|
|
||||||
$enabled = !empty($settings['baidu_push_enabled']) || !empty($settings['seo_baidu_push']);
|
|
||||||
$token = (string) ($settings['baidu_push_token'] ?? '');
|
|
||||||
if ($token === '' && !empty($settings['seo_baidu_token'])) {
|
|
||||||
$token = (string) $settings['seo_baidu_token'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'baidu_push_enabled' => $enabled,
|
'baidu_push_enabled' => !empty($settings['baidu_push_enabled']),
|
||||||
'baidu_push_token' => $token,
|
'baidu_push_token' => (string) ($settings['baidu_push_token'] ?? ''),
|
||||||
'baidu_push_auto' => !isset($settings['baidu_push_auto']) || !empty($settings['baidu_push_auto']),
|
'baidu_push_auto' => !isset($settings['baidu_push_auto']) || !empty($settings['baidu_push_auto']),
|
||||||
|
'baidu_push_normal' => !isset($settings['baidu_push_normal']) || !empty($settings['baidu_push_normal']),
|
||||||
|
'baidu_push_fast' => !empty($settings['baidu_push_fast']),
|
||||||
'baidu_push_on_publish' => !isset($settings['baidu_push_on_publish']) || !empty($settings['baidu_push_on_publish']),
|
'baidu_push_on_publish' => !isset($settings['baidu_push_on_publish']) || !empty($settings['baidu_push_on_publish']),
|
||||||
'baidu_push_on_update' => !empty($settings['baidu_push_on_update']),
|
'baidu_push_on_update' => !empty($settings['baidu_push_on_update']),
|
||||||
'baidu_push_batch_size' => max(1, min(500, (int) ($settings['baidu_push_batch_size'] ?? 50))),
|
|
||||||
'baidu_push_log_enabled' => !isset($settings['baidu_push_log_enabled']) || !empty($settings['baidu_push_log_enabled']),
|
'baidu_push_log_enabled' => !isset($settings['baidu_push_log_enabled']) || !empty($settings['baidu_push_log_enabled']),
|
||||||
'baidu_push_verified' => !empty($settings['baidu_push_verified']),
|
|
||||||
'xzh_on' => !empty($settings['xzh_on']),
|
|
||||||
'xzh_appid' => (string) ($settings['xzh_appid'] ?? ''),
|
|
||||||
'xzh_render_tail' => !isset($settings['xzh_render_tail']) || !empty($settings['xzh_render_tail']),
|
|
||||||
'xzh_jsonld_single' => !isset($settings['xzh_jsonld_single']) || !empty($settings['xzh_jsonld_single']),
|
|
||||||
'xzh_jsonld_page' => !empty($settings['xzh_jsonld_page']),
|
|
||||||
'xzh_jsonld_img' => !empty($settings['xzh_jsonld_img']),
|
|
||||||
'seo_baidu_normal' => !empty($settings['seo_baidu_normal']),
|
|
||||||
'seo_baidu_fast' => !empty($settings['seo_baidu_fast']),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,42 +210,6 @@ class HomeCms {
|
|||||||
return $settings['home_exclude_cats'];
|
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 {
|
private static function normalize_sort_options(array $raw): array {
|
||||||
$allowed = self::homepage_allowed_sorts();
|
$allowed = self::homepage_allowed_sorts();
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|||||||
+206
-37
@@ -121,7 +121,9 @@ class PaymentRuntime {
|
|||||||
],
|
],
|
||||||
'vip' => [
|
'vip' => [
|
||||||
'enabled' => !empty($settings['pay_vip_pass_charge_s']),
|
'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。
|
// 默认非单密码(卡号+密码),对齐参考主题 pay_vip_pass_charge_only_password 默认 false。
|
||||||
'only_password' => !empty($settings['pay_vip_pass_charge_only_password']),
|
'only_password' => !empty($settings['pay_vip_pass_charge_only_password']),
|
||||||
],
|
],
|
||||||
@@ -171,33 +173,21 @@ class PaymentRuntime {
|
|||||||
$name1 = trim((string) ($settings['pay_user_vip_1_name'] ?? ''));
|
$name1 = trim((string) ($settings['pay_user_vip_1_name'] ?? ''));
|
||||||
$name2 = trim((string) ($settings['pay_user_vip_2_name'] ?? ''));
|
$name2 = trim((string) ($settings['pay_user_vip_2_name'] ?? ''));
|
||||||
if ($name1 === '') {
|
if ($name1 === '') {
|
||||||
$name1 = __('一级会员', 'aksrt');
|
$name1 = __('黄金会员', 'aksrt');
|
||||||
}
|
}
|
||||||
if ($name2 === '') {
|
if ($name2 === '') {
|
||||||
$name2 = __('二级会员', 'aksrt');
|
$name2 = __('钻石会员', 'aksrt');
|
||||||
}
|
}
|
||||||
|
|
||||||
$badge = $vip2_enabled ? $name2 : ($vip1_enabled ? $name1 : __('VIP', 'aksrt'));
|
$badge = $vip2_enabled ? $name2 : ($vip1_enabled ? $name1 : __('VIP', 'aksrt'));
|
||||||
|
|
||||||
$renew_price_type = sanitize_key((string) ($settings['vip_renew_price_type'] ?? 'discount'));
|
$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';
|
$renew_price_type = 'discount';
|
||||||
}
|
}
|
||||||
|
|
||||||
$default_l1 = "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡";
|
$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('开通VIP会员,享受会员专属折扣以及多项特权', 'aksrt')));
|
||||||
$default_l2 = "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡";
|
$more = (string) ($settings['pay_user_vip_more'] ?? '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>');
|
||||||
$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 = [
|
$allowed_equity = [
|
||||||
'li' => [],
|
'li' => [],
|
||||||
'ul' => [],
|
'ul' => [],
|
||||||
@@ -218,27 +208,30 @@ class PaymentRuntime {
|
|||||||
'name' => $vip1_enabled ? $name1 : $badge,
|
'name' => $vip1_enabled ? $name1 : $badge,
|
||||||
'badge_text' => $badge,
|
'badge_text' => $badge,
|
||||||
'desc' => $desc,
|
'desc' => $desc,
|
||||||
'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]),
|
// 管理员编辑的富文本说明:与保存端同为 post 级 HTML(wp_kses_post),
|
||||||
'help_text' => $desc !== '' ? $desc : __('会员权益以免扣下载和身份展示为主,开通后立即生效。', 'aksrt'),
|
// 不再用权益列表的窄白名单二次剥标签(h/img/hr/table 等)。
|
||||||
|
'more' => wp_kses_post($more),
|
||||||
'levels' => [
|
'levels' => [
|
||||||
1 => [
|
1 => [
|
||||||
'enabled' => $vip1_enabled,
|
'enabled' => $vip1_enabled,
|
||||||
'name' => $name1,
|
'name' => $name1,
|
||||||
'desc' => sanitize_text_field((string) ($settings['pay_user_vip_1_desc'] ?? __('尊享一级会员特权', 'aksrt'))),
|
'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),
|
'equity' => wp_kses((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>'), $allowed_equity),
|
||||||
'img_icon' => self::get_vip_icon_url(1, (string) ($settings['vip_1_img_icon'] ?? '')),
|
'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']),
|
'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))),
|
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_1'] ?? 0))),
|
||||||
],
|
],
|
||||||
2 => [
|
2 => [
|
||||||
'enabled' => $vip2_enabled,
|
'enabled' => $vip2_enabled,
|
||||||
'name' => $name2,
|
'name' => $name2,
|
||||||
'desc' => sanitize_text_field((string) ($settings['pay_user_vip_2_desc'] ?? __('尊享二级会员特权', 'aksrt'))),
|
'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),
|
'equity' => wp_kses((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>'), $allowed_equity),
|
||||||
'img_icon' => self::get_vip_icon_url(2, (string) ($settings['vip_2_img_icon'] ?? '')),
|
'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']),
|
'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))),
|
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_2'] ?? 0))),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -247,21 +240,16 @@ class PaymentRuntime {
|
|||||||
],
|
],
|
||||||
'renew' => [
|
'renew' => [
|
||||||
'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']),
|
'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']),
|
||||||
'desc' => sanitize_textarea_field((string) ($settings['vip_renew_desc'] ?? '')),
|
'desc' => sanitize_text_field((string) ($settings['vip_renew_desc'] ?? '立即续费会员,畅享VIP权益')),
|
||||||
'price_type' => $renew_price_type,
|
'price_type' => $renew_price_type,
|
||||||
'discount' => max(1, min(100, (int) ($settings['vip_renew_discount'] ?? 8))),
|
'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', 999900),
|
'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 50000),
|
||||||
],
|
|
||||||
'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'] ?? '')),
|
|
||||||
],
|
],
|
||||||
|
'upgrade' => self::normalize_vip_upgrade_settings($settings),
|
||||||
'points_exchange' => [
|
'points_exchange' => [
|
||||||
'enabled' => !empty($settings['pay_vip_points_exchange_s']),
|
'enabled' => !empty($settings['pay_vip_points_exchange_s']),
|
||||||
'desc' => sanitize_textarea_field((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
|
'desc' => wp_kses_post((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
|
||||||
'packages_raw' => sanitize_textarea_field((string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久")),
|
'packages' => self::normalize_vip_points_packages($settings['pay_vip_points_exchange_product'] ?? null),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -728,4 +716,185 @@ class PaymentRuntime {
|
|||||||
}
|
}
|
||||||
return implode("\n", $lines);
|
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' => sanitize_text_field((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 === '') {
|
if ($name === '' || $style_name === '' || $label === '' || $url === '') {
|
||||||
continue;
|
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>';
|
return $html . '</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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; ?>
|
<?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>
|
</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; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Baidu push harness.
|
||||||
|
*
|
||||||
|
* Drives the REAL SearchPush / SearchPushMetaBox against WP stubs so the
|
||||||
|
* reference-parity contract is covered end to end:
|
||||||
|
* - normal / daily channels hit data.zz.baidu.com (daily appends type=daily);
|
||||||
|
* - successful pushes are recorded in meta and never re-submitted;
|
||||||
|
* - failed pushes retry on update when baidu_push_on_update is on;
|
||||||
|
* - the 「重新提交」checkbox forces a push only with a valid metabox nonce;
|
||||||
|
* - terms push via saved_term (post_format and other taxonomies excluded);
|
||||||
|
* - the edit panel renders status lines, update time and the resubmit checkbox.
|
||||||
|
*
|
||||||
|
* Usage: php baidu-push-harness.php -> prints "Baidu push harness passed".
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
define('ABSPATH', __DIR__ . '/');
|
||||||
|
define('WP_DEBUG', false);
|
||||||
|
|
||||||
|
final class WP_Error {
|
||||||
|
public function __construct(private string $code, private string $message) {}
|
||||||
|
public function get_error_message(): string { return $this->message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WP_Post {
|
||||||
|
public int $ID = 0;
|
||||||
|
public string $post_type = 'post';
|
||||||
|
public string $post_status = 'publish';
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WP_Term {
|
||||||
|
public int $term_id = 0;
|
||||||
|
public string $taxonomy = 'category';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- WP function stubs ----------
|
||||||
|
function add_action(...$args) {}
|
||||||
|
function apply_filters($tag, $value) { return $value; }
|
||||||
|
function __($s, $d = null) { return (string) $s; }
|
||||||
|
function esc_html($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
|
||||||
|
function esc_attr($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
|
||||||
|
function esc_html_e($s, $d = null) { echo esc_html($s); }
|
||||||
|
function wp_json_encode($v, $f = 0) { return (string) json_encode($v, $f); }
|
||||||
|
function wp_nonce_field($action, $name) { echo '<input type="hidden" name="' . $name . '" value="nonce" />'; }
|
||||||
|
function wp_unslash($s) { return $s; }
|
||||||
|
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
|
||||||
|
function wp_verify_nonce($nonce, $action) { return $nonce === 'valid-nonce' && $action === 'aksrt_push_meta_save'; }
|
||||||
|
function is_wp_error($t) { return $t instanceof WP_Error; }
|
||||||
|
function home_url($path = '/') { return 'https://example.test' . $path; }
|
||||||
|
function current_time($type = 'mysql') { return '2026-10-13 12:00:00'; }
|
||||||
|
function get_permalink($post) { return 'https://example.test/?p=' . (int) $post->ID; }
|
||||||
|
function get_term_link($term, $taxonomy = '') {
|
||||||
|
$id = is_object($term) ? (int) $term->term_id : (int) $term;
|
||||||
|
return 'https://example.test/?cat=' . $id;
|
||||||
|
}
|
||||||
|
function wp_is_post_revision($id) { return false; }
|
||||||
|
function wp_is_post_autosave($id) { return false; }
|
||||||
|
function aksrt_searchable_post_types(): array { return ['post']; }
|
||||||
|
function get_object_taxonomies($types, $output = 'names') { return ['post_tag', 'category', 'post_format']; }
|
||||||
|
|
||||||
|
// Outgoing HTTP: every call is captured so channel/URL/body can be asserted.
|
||||||
|
$GLOBALS['push_requests'] = [];
|
||||||
|
function wp_remote_post($url, $args = []) {
|
||||||
|
$GLOBALS['push_requests'][] = ['url' => (string) $url, 'body' => (string) ($args['body'] ?? '')];
|
||||||
|
return ['response' => ['code' => 200], 'body' => (string) json_encode(['success' => 1, 'remain' => 99])];
|
||||||
|
}
|
||||||
|
function wp_remote_retrieve_body($r) { return is_array($r) ? (string) ($r['body'] ?? '') : ''; }
|
||||||
|
function wp_remote_retrieve_response_code($r) { return is_array($r) ? (int) ($r['response']['code'] ?? 0) : 0; }
|
||||||
|
|
||||||
|
// In-memory meta stores.
|
||||||
|
$GLOBALS['push_post_meta'] = [];
|
||||||
|
$GLOBALS['push_term_meta'] = [];
|
||||||
|
function get_post_meta($id, $key, $single = false) { return $GLOBALS['push_post_meta'][$id][$key] ?? ''; }
|
||||||
|
function update_post_meta($id, $key, $value) { $GLOBALS['push_post_meta'][$id][$key] = $value; return true; }
|
||||||
|
function delete_post_meta($id, $key) { unset($GLOBALS['push_post_meta'][$id][$key]); return true; }
|
||||||
|
function get_term_meta($id, $key, $single = false) { return $GLOBALS['push_term_meta'][$id][$key] ?? ''; }
|
||||||
|
function update_term_meta($id, $key, $value) { $GLOBALS['push_term_meta'][$id][$key] = $value; return true; }
|
||||||
|
function delete_term_meta($id, $key) { unset($GLOBALS['push_term_meta'][$id][$key]); return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace AKSRT\Settings {
|
||||||
|
class Settings {
|
||||||
|
public static array $config = [];
|
||||||
|
public static function get_search_push_settings(): array { return self::$config; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPush.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPushMetaBox.php';
|
||||||
|
|
||||||
|
use AKSRT\Extensions\SearchPush;
|
||||||
|
use AKSRT\Extensions\SearchPushMetaBox;
|
||||||
|
|
||||||
|
$failures = [];
|
||||||
|
$check = static function (string $label, bool $ok) use (&$failures): void {
|
||||||
|
if (!$ok) {
|
||||||
|
$failures[] = $label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$reset = static function (array $config): void {
|
||||||
|
\AKSRT\Settings\Settings::$config = $config;
|
||||||
|
$GLOBALS['push_requests'] = [];
|
||||||
|
$GLOBALS['push_post_meta'] = [];
|
||||||
|
$GLOBALS['push_term_meta'] = [];
|
||||||
|
$_POST = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
$post = static function (int $id): WP_Post {
|
||||||
|
$p = new WP_Post();
|
||||||
|
$p->ID = $id;
|
||||||
|
return $p;
|
||||||
|
};
|
||||||
|
|
||||||
|
$defaults = [
|
||||||
|
'baidu_push_enabled' => true,
|
||||||
|
'baidu_push_token' => 'TOKEN',
|
||||||
|
'baidu_push_auto' => true,
|
||||||
|
'baidu_push_on_publish' => true,
|
||||||
|
'baidu_push_on_update' => false,
|
||||||
|
'baidu_push_normal' => true,
|
||||||
|
'baidu_push_fast' => false,
|
||||||
|
'baidu_push_log_enabled' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$push = SearchPush::get_instance();
|
||||||
|
$api = 'http://data.zz.baidu.com/urls?site=' . rawurlencode('https://example.test/') . '&token=TOKEN';
|
||||||
|
|
||||||
|
// 1) First publish pushes once through the normal-collection endpoint.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(11));
|
||||||
|
$check('首次发布推送一次', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$check('普通收录请求地址正确', ($GLOBALS['push_requests'][0]['url'] ?? '') === $api);
|
||||||
|
$check('普通收录请求体为文章链接', ($GLOBALS['push_requests'][0]['body'] ?? '') === 'https://example.test/?p=11');
|
||||||
|
$record = SearchPush::get_push_record(11);
|
||||||
|
$check('推送记录写入 meta', !empty($record['normal_push']));
|
||||||
|
$check('推送记录带更新时间', !empty($record['update_time']));
|
||||||
|
$check('推送记录不含快速收录', !array_key_exists('daily_push', $record));
|
||||||
|
|
||||||
|
// 2) A completed push is never re-submitted on save.
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(11));
|
||||||
|
$check('成功后更新不重复推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
|
||||||
|
// 3) Failed push retries on update when the update switch is on.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_on_update' => true]));
|
||||||
|
SearchPush::save_push_record(12, ['normal_push' => false, 'normal_result' => null, 'update_time' => '2026-10-13 09:00:00']);
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(12));
|
||||||
|
$check('失败后更新重试一次', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$check('重试后记录为成功', !empty(SearchPush::get_push_record(12)['normal_push']));
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(12));
|
||||||
|
$check('重试成功后不重复推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
|
||||||
|
// 4) 「重新提交」with a valid nonce forces a push even after success.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(13));
|
||||||
|
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
|
||||||
|
$_POST[SearchPush::NONCE_FIELD] = 'valid-nonce';
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(13));
|
||||||
|
$check('重新提交强制再次推送', count($GLOBALS['push_requests']) === 2);
|
||||||
|
|
||||||
|
// 5) A forged nonce is ignored, dedup still applies.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(14));
|
||||||
|
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
|
||||||
|
$_POST[SearchPush::NONCE_FIELD] = 'forged-nonce';
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(14));
|
||||||
|
$check('无效 nonce 的重新提交被忽略', count($GLOBALS['push_requests']) === 1);
|
||||||
|
|
||||||
|
// 6) Fast collection appends type=daily and records both channels.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_fast' => true]));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(15));
|
||||||
|
$check('双通道推送两次', count($GLOBALS['push_requests']) === 2);
|
||||||
|
$check('快速收录走 type=daily', str_contains($GLOBALS['push_requests'][1]['url'] ?? '', '&type=daily'));
|
||||||
|
$record15 = SearchPush::get_push_record(15);
|
||||||
|
$check('双通道记录完整', !empty($record15['normal_push']) && !empty($record15['daily_push']));
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(15));
|
||||||
|
$check('双通道成功后不重复推送', count($GLOBALS['push_requests']) === 2);
|
||||||
|
|
||||||
|
// 7) No channel enabled: nothing pushed, nothing recorded.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_normal' => false]));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(16));
|
||||||
|
$check('通道全关不推送', count($GLOBALS['push_requests']) === 0);
|
||||||
|
$check('通道全关不写记录', SearchPush::get_push_record(16) === []);
|
||||||
|
|
||||||
|
// 8) Feature off / token missing: nothing pushed, nothing recorded.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_enabled' => false]));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(17));
|
||||||
|
$check('未启用不推送', count($GLOBALS['push_requests']) === 0);
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_token' => '']));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(18));
|
||||||
|
$check('缺少 Token 不推送', count($GLOBALS['push_requests']) === 0);
|
||||||
|
$check('缺少 Token 不写记录', SearchPush::get_push_record(18) === []);
|
||||||
|
|
||||||
|
// 9) Terms push via saved_term with the same dedup / resubmit rules.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_saved_term(21, 21, 'category');
|
||||||
|
$check('术语推送一次', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$check('术语推送链接正确', ($GLOBALS['push_requests'][0]['body'] ?? '') === 'https://example.test/?cat=21');
|
||||||
|
$check('术语记录写入', !empty(SearchPush::get_push_record(21, true)['normal_push']));
|
||||||
|
$push->on_saved_term(21, 21, 'category');
|
||||||
|
$check('术语成功后不重复推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$push->on_saved_term(22, 22, 'post_format');
|
||||||
|
$check('post_format 术语不推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
|
||||||
|
$_POST[SearchPush::NONCE_FIELD] = 'valid-nonce';
|
||||||
|
$push->on_saved_term(21, 21, 'category');
|
||||||
|
$check('术语重新提交强制推送', count($GLOBALS['push_requests']) === 2);
|
||||||
|
|
||||||
|
// 10) Shared type / taxonomy policy.
|
||||||
|
$check('可推送文章类型', SearchPush::pushable_post_types() === ['post']);
|
||||||
|
$check('可推送分类法排除 post_format', SearchPush::pushable_taxonomies() === ['post_tag', 'category']);
|
||||||
|
|
||||||
|
// 11) Edit panel rendering.
|
||||||
|
$reset($defaults);
|
||||||
|
SearchPush::save_push_record(31, [
|
||||||
|
'normal_push' => true,
|
||||||
|
'normal_result' => ['success' => 1],
|
||||||
|
'daily_push' => false,
|
||||||
|
'daily_result' => null,
|
||||||
|
'update_time' => '2026-10-13 09:00:00',
|
||||||
|
]);
|
||||||
|
SearchPush::save_push_record(31, [
|
||||||
|
'normal_push' => true,
|
||||||
|
'normal_result' => ['success' => 1],
|
||||||
|
'daily_push' => false,
|
||||||
|
'daily_result' => null,
|
||||||
|
'update_time' => '2026-10-13 09:00:00',
|
||||||
|
], true);
|
||||||
|
$box = SearchPushMetaBox::get_instance();
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$box->render_post_panel($post(31));
|
||||||
|
$html = (string) ob_get_clean();
|
||||||
|
$check('面板含普通收录成功', str_contains($html, '普通收录:成功'));
|
||||||
|
$check('面板含快速收录失败', str_contains($html, '快速收录:失败'));
|
||||||
|
$check('面板含更新时间', str_contains($html, '2026-10-13 09:00:00'));
|
||||||
|
$check('面板含重新提交勾选', str_contains($html, 'name="aksrt_push_resubmit"'));
|
||||||
|
$check('面板含 nonce 字段', str_contains($html, 'name="aksrt_push_meta_nonce"'));
|
||||||
|
$check('面板转义接口 JSON', str_contains($html, '{"success":1}'));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$box->render_term_panel(null);
|
||||||
|
$addHtml = (string) ob_get_clean();
|
||||||
|
$check('新增术语表单仅提示', str_contains($addHtml, '发布、更新后刷新页面后可查看提交结果') && !str_contains($addHtml, '<tr'));
|
||||||
|
|
||||||
|
$term = new WP_Term();
|
||||||
|
$term->term_id = 31;
|
||||||
|
ob_start();
|
||||||
|
$box->render_term_panel($term);
|
||||||
|
$termHtml = (string) ob_get_clean();
|
||||||
|
$check('术语编辑表单为表单行', str_contains($termHtml, '<tr class="form-field">'));
|
||||||
|
$check('术语编辑表单含重新提交', str_contains($termHtml, 'name="aksrt_push_resubmit"'));
|
||||||
|
|
||||||
|
if ($failures) {
|
||||||
|
echo "Baidu push harness FAILED\n";
|
||||||
|
foreach ($failures as $failure) {
|
||||||
|
echo ' - ' . $failure . "\n";
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Baidu push harness passed\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「百度推送」后台设置页契约(对齐参考主题 zibll):
|
||||||
|
* 1) PHP schema(SchemaExtensionsPush.php)与 JS schema(push.js)字段键、顺序、默认值一致;
|
||||||
|
* 2) 参考主题字段全集存在:自动推送 JS、普通收录、快速收录、准入 Token、发布/更新触发;
|
||||||
|
* 3) 双路/迁移字段(SEO 页 seo_baidu_*、熊掌号 xzh_*、批量推送 batch_*)已彻底移除,无双路兼容;
|
||||||
|
* 4) 运行时(SearchPush + ExtensionRuntime)只读取 canonical 键,快速收录走 type=daily;
|
||||||
|
* 5) 分类法 saved_term 推送、meta 去重、「重新提交」nonce 与编辑页面板均已接入,
|
||||||
|
* 并由 PHP harness 驱动真实逻辑验证。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const php = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaExtensionsPush.php'), 'utf8');
|
||||||
|
const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/extensions/push.js'), 'utf8');
|
||||||
|
const runtime = fs.readFileSync(path.join(root, 'src/Extensions/SearchPush.php'), 'utf8');
|
||||||
|
const extensionRuntime = fs.readFileSync(path.join(root, 'src/Settings/ExtensionRuntime.php'), 'utf8');
|
||||||
|
const globalPhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/GlobalSchema.php'), 'utf8');
|
||||||
|
const seoJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/global/seo.js'), 'utf8');
|
||||||
|
const displayGroups = fs.readFileSync(path.join(root, 'src/Settings/DisplayPageGroups.php'), 'utf8');
|
||||||
|
const metaBox = fs.readFileSync(path.join(root, 'src/Extensions/SearchPushMetaBox.php'), 'utf8');
|
||||||
|
const functions = fs.readFileSync(path.join(root, 'functions.php'), 'utf8');
|
||||||
|
|
||||||
|
// 参考主题(zibll)百度推送字段全集,AKSRT 单一 canonical 配置面(扩展&增强 → 百度推送)。
|
||||||
|
const REFERENCE_KEYS = [
|
||||||
|
'baidu_push_enabled',
|
||||||
|
'baidu_push_token',
|
||||||
|
'baidu_push_auto',
|
||||||
|
'baidu_push_on_publish',
|
||||||
|
'baidu_push_on_update',
|
||||||
|
'baidu_push_normal',
|
||||||
|
'baidu_push_fast',
|
||||||
|
'baidu_push_log_enabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 双路 / 迁移 / 死代码字段:SEO 页 seo_baidu_*、熊掌号 xzh_*、批量推送与校验占位。
|
||||||
|
const DUAL_KEYS = [
|
||||||
|
'seo_baidu_push',
|
||||||
|
'seo_baidu_normal',
|
||||||
|
'seo_baidu_fast',
|
||||||
|
'seo_baidu_token',
|
||||||
|
'xzh_on',
|
||||||
|
'xzh_appid',
|
||||||
|
'xzh_render_tail',
|
||||||
|
'xzh_jsonld_single',
|
||||||
|
'xzh_jsonld_page',
|
||||||
|
'xzh_jsonld_img',
|
||||||
|
'baidu_push_batch_size',
|
||||||
|
'baidu_push_verified',
|
||||||
|
'push_batch',
|
||||||
|
'batch_size',
|
||||||
|
];
|
||||||
|
|
||||||
|
function phpFieldKeys(src) {
|
||||||
|
const found = [];
|
||||||
|
const re = /'key'\s*=>\s*'([a-z0-9_]+)',\s*'label'/g;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(src)) !== null) found.push(m[1]);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsFieldKeys(src) {
|
||||||
|
const found = [];
|
||||||
|
const re = /key:\s*'([a-z0-9_]+)',\s*label:/g;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(src)) !== null) found.push(m[1]);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('PHP push schema 字段键与顺序对齐参考主题', () => {
|
||||||
|
assert.deepEqual(phpFieldKeys(php), REFERENCE_KEYS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JS push schema 字段键与顺序对齐参考主题', () => {
|
||||||
|
assert.deepEqual(jsFieldKeys(js), REFERENCE_KEYS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PHP schema 默认值对齐', () => {
|
||||||
|
assert.match(php, /'baidu_push_enabled',\s*'label'\s*=>\s*'启用百度推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
|
||||||
|
assert.match(php, /'baidu_push_token',\s*'label'\s*=>\s*'推送 Token',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*''/);
|
||||||
|
assert.match(php, /'baidu_push_auto',\s*'label'\s*=>\s*'自动推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
assert.match(php, /'baidu_push_on_publish',\s*'label'\s*=>\s*'发布时主动推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
assert.match(php, /'baidu_push_on_update',\s*'label'\s*=>\s*'更新时推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
|
||||||
|
assert.match(php, /'baidu_push_normal',\s*'label'\s*=>\s*'普通收录',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
assert.match(php, /'baidu_push_fast',\s*'label'\s*=>\s*'快速收录',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
|
||||||
|
assert.match(php, /'baidu_push_log_enabled',\s*'label'\s*=>\s*'记录推送日志',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JS schema 默认值对齐', () => {
|
||||||
|
assert.match(js, /baidu_push_enabled:\s*false/);
|
||||||
|
assert.match(js, /baidu_push_token:\s*''/);
|
||||||
|
assert.match(js, /baidu_push_auto:\s*true/);
|
||||||
|
assert.match(js, /baidu_push_on_publish:\s*true/);
|
||||||
|
assert.match(js, /baidu_push_on_update:\s*false/);
|
||||||
|
assert.match(js, /baidu_push_normal:\s*true/);
|
||||||
|
assert.match(js, /baidu_push_fast:\s*false/);
|
||||||
|
assert.match(js, /baidu_push_log_enabled:\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('双路/迁移/死代码字段在 schema、运行时与展示层均已移除(无双路)', () => {
|
||||||
|
for (const legacy of DUAL_KEYS) {
|
||||||
|
assert.ok(!php.includes(legacy), `SchemaExtensionsPush 残留: ${legacy}`);
|
||||||
|
assert.ok(!js.includes(legacy), `push.js 残留: ${legacy}`);
|
||||||
|
assert.ok(!runtime.includes(legacy), `SearchPush 残留: ${legacy}`);
|
||||||
|
assert.ok(!extensionRuntime.includes(legacy), `ExtensionRuntime 残留: ${legacy}`);
|
||||||
|
assert.ok(!globalPhp.includes(legacy), `GlobalSchema 残留: ${legacy}`);
|
||||||
|
assert.ok(!seoJs.includes(legacy), `seo.js 残留: ${legacy}`);
|
||||||
|
assert.ok(!displayGroups.includes(legacy), `DisplayPageGroups 残留: ${legacy}`);
|
||||||
|
assert.ok(!metaBox.includes(legacy), `SearchPushMetaBox 残留: ${legacy}`);
|
||||||
|
assert.ok(!functions.includes(legacy), `functions.php 残留: ${legacy}`);
|
||||||
|
}
|
||||||
|
assert.ok(!globalPhp.includes("'baidu'"), 'GlobalSchema SEO 分组残留百度重复配置面');
|
||||||
|
assert.ok(!seoJs.includes("key: 'baidu'"), 'seo.js 残留百度重复配置面');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('普通/快速收录通道接入百度链接提交接口', () => {
|
||||||
|
assert.match(runtime, /data\.zz\.baidu\.com\/urls\?site=/);
|
||||||
|
assert.match(runtime, /'baidu_push_normal'/);
|
||||||
|
assert.match(runtime, /'baidu_push_fast'/);
|
||||||
|
assert.match(runtime, /&type=daily/);
|
||||||
|
assert.match(extensionRuntime, /'baidu_push_normal'\s*=>/);
|
||||||
|
assert.match(extensionRuntime, /'baidu_push_fast'\s*=>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('前台自动推送 JS 与参考主题一致', () => {
|
||||||
|
assert.match(runtime, /zz\.bdstatic\.com\/linksubmit\/push\.js/);
|
||||||
|
assert.match(runtime, /push\.zhanzhang\.baidu\.com\/push\.js/);
|
||||||
|
assert.match(runtime, /add_action\('wp_footer'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('发布/更新触发沿用 transition_post_status 且仅限可检索内容类型', () => {
|
||||||
|
assert.match(runtime, /transition_post_status/);
|
||||||
|
assert.match(runtime, /aksrt_searchable_post_types/);
|
||||||
|
assert.match(runtime, /wp_is_post_revision/);
|
||||||
|
assert.match(runtime, /wp_is_post_autosave/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('推送日志开关接入运行时', () => {
|
||||||
|
assert.match(runtime, /baidu_push_log_enabled/);
|
||||||
|
assert.match(extensionRuntime, /'baidu_push_log_enabled'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('服务状态契约保留扩展总览消费的键', () => {
|
||||||
|
for (const key of ['enabled', 'provider', 'configured', 'available', 'verified', 'transport', 'missing_fields']) {
|
||||||
|
assert.ok(runtime.includes(`'${key}'`), `get_status 缺少 ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('分类法推送接入 saved_term 且排除文章形式', () => {
|
||||||
|
assert.match(runtime, /add_action\('saved_term'/);
|
||||||
|
assert.match(runtime, /public static function pushable_taxonomies/);
|
||||||
|
assert.match(runtime, /array_diff\(\(array\) \$taxonomies, \['post_format', 'nav_menu'\]\)/);
|
||||||
|
assert.match(runtime, /aksrt_baidu_push_taxonomies/);
|
||||||
|
assert.match(runtime, /get_term_link\(\$term_id, \$taxonomy\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('推送结果写入 meta 并按记录去重', () => {
|
||||||
|
assert.match(runtime, /META_KEY\s*=\s*'_aksrt_baidu_push_back'/);
|
||||||
|
assert.match(runtime, /private function record_is_complete/);
|
||||||
|
assert.match(runtime, /private function build_record/);
|
||||||
|
assert.match(runtime, /'normal_push'/);
|
||||||
|
assert.match(runtime, /'daily_push'/);
|
||||||
|
assert.match(runtime, /'update_time'/);
|
||||||
|
assert.match(runtime, /update_term_meta\(/);
|
||||||
|
assert.match(runtime, /delete_term_meta\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('「重新提交」勾选需通过 metabox nonce 校验', () => {
|
||||||
|
assert.match(runtime, /NONCE_ACTION\s*=\s*'aksrt_push_meta_save'/);
|
||||||
|
assert.match(runtime, /NONCE_FIELD\s*=\s*'aksrt_push_meta_nonce'/);
|
||||||
|
assert.match(runtime, /RESUBMIT_FIELD\s*=\s*'aksrt_push_resubmit'/);
|
||||||
|
assert.match(runtime, /wp_verify_nonce\(\$nonce, self::NONCE_ACTION\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('编辑页面板与参考主题「百度资源提交」Metabox 对齐', () => {
|
||||||
|
assert.match(metaBox, /add_meta_box\(\s*'aksrt_baidu_push',\s*__\('百度资源提交', 'aksrt'\)/);
|
||||||
|
assert.match(metaBox, /'side',\s*'high'/);
|
||||||
|
assert.match(metaBox, /\{\$taxonomy\}_edit_form_fields/);
|
||||||
|
assert.match(metaBox, /\{\$taxonomy\}_add_form_fields/);
|
||||||
|
assert.match(metaBox, /普通收录:成功/);
|
||||||
|
assert.match(metaBox, /快速收录:失败/);
|
||||||
|
assert.match(metaBox, /更新时间:/);
|
||||||
|
assert.match(metaBox, /重新提交/);
|
||||||
|
assert.match(metaBox, /发布、更新后刷新页面后可查看提交结果/);
|
||||||
|
assert.match(metaBox, /wp_nonce_field\(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD\)/);
|
||||||
|
assert.match(metaBox, /esc_html\(\$line\[1\]\)/, '接口结果需转义后输出');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('functions.php 接线推送面板', () => {
|
||||||
|
assert.match(functions, /SearchPushMetaBox::get_instance\(\)->init\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('百度推送行为 harness 通过', () => {
|
||||||
|
const harness = path.join(root, 'tests', 'admin', 'baidu-push-harness.php');
|
||||||
|
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||||
|
assert.match(output, /Baidu push harness passed/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WeChat template-message settings harness.
|
||||||
|
*
|
||||||
|
* Drives the REAL UserController REST callbacks (get_wechat_status /
|
||||||
|
* update_wechat_settings) against WP stubs so the previously-broken routes are
|
||||||
|
* covered end to end:
|
||||||
|
* - GET /wechat/status returns the WeChatNotify status shape (masked secret);
|
||||||
|
* - POST /wechat/settings persists through Integration's key whitelist;
|
||||||
|
* - the *** mask keeps the stored secret instead of overwriting it;
|
||||||
|
* - omitted fields fall back to current values; unknown keys are dropped;
|
||||||
|
* - inputs are sanitized before storage.
|
||||||
|
*
|
||||||
|
* Usage: php wechat-settings-harness.php -> prints "WeChat settings harness passed".
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
define('ABSPATH', __DIR__ . '/');
|
||||||
|
|
||||||
|
final class WP_REST_Response {
|
||||||
|
public $data;
|
||||||
|
public $status;
|
||||||
|
public function __construct($data = null, int $status = 200) {
|
||||||
|
$this->data = $data;
|
||||||
|
$this->status = $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WP_REST_Request {
|
||||||
|
private array $json;
|
||||||
|
public function __construct(array $json = []) { $this->json = $json; }
|
||||||
|
public function get_json_params(): array { return $this->json; }
|
||||||
|
public function get_method(): string { return 'POST'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- WP function stubs ----------
|
||||||
|
function __($s, $d = null) { return (string) $s; }
|
||||||
|
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
|
||||||
|
function current_user_can($c) { return true; }
|
||||||
|
function wp_send_json_success($d) { echo (string) json_encode($d); }
|
||||||
|
function get_transient($k) { return $GLOBALS['wechat_transient'] ?? false; }
|
||||||
|
function get_option($k, $default = false) { return $GLOBALS['wechat_options'][$k] ?? $default; }
|
||||||
|
function update_option($k, $v) { $GLOBALS['wechat_options'][$k] = $v; return true; }
|
||||||
|
|
||||||
|
$GLOBALS['wechat_options'] = ['aksrt_wechat_settings' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'app_id' => 'wxAPPID',
|
||||||
|
'app_secret' => 'SECRET123',
|
||||||
|
'template_id' => 'TPL1',
|
||||||
|
]];
|
||||||
|
$GLOBALS['wechat_transient'] = 'ACCESS_TOKEN';
|
||||||
|
|
||||||
|
$GLOBALS['wpdb'] = new class {
|
||||||
|
public $usermeta = 'wp_usermeta';
|
||||||
|
public function get_var($sql) { return 7; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace AKSRT\Settings {
|
||||||
|
// Settings 层仅为薄委托,harness 直接对接真实 Integration。
|
||||||
|
class Settings {
|
||||||
|
public static function get_wechat_settings(): array { return Integration::get_wechat_settings(); }
|
||||||
|
public static function update_wechat_settings(array $settings): void { Integration::update_wechat_settings($settings); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/ControllerBase.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerReportOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerAuthPhoneOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerRestrictionOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerFollowOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerProfileMediaOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserController.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Settings/Integration.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Settings/DomainGroups.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Payment/WeChatNotify.php';
|
||||||
|
|
||||||
|
$failures = [];
|
||||||
|
$check = static function (string $label, bool $ok) use (&$failures): void {
|
||||||
|
if (!$ok) {
|
||||||
|
$failures[] = $label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$stored = static function (): array {
|
||||||
|
$v = get_option('aksrt_wechat_settings', []);
|
||||||
|
return is_array($v) ? $v : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
$controller = \AKSRT\Admin\UserController::get_instance();
|
||||||
|
|
||||||
|
// 1) GET /wechat/status 返回完整状态形状(密钥掩码)。
|
||||||
|
$response = $controller->get_wechat_status(new WP_REST_Request());
|
||||||
|
$data = is_array($response->data) ? ($response->data['data'] ?? []) : [];
|
||||||
|
$check('status 含 enabled', array_key_exists('enabled', $data) && !empty($data['enabled']));
|
||||||
|
$check('status 含 appid', ($data['appid'] ?? '') === 'wxAPPID');
|
||||||
|
$check('status configured', !empty($data['configured']));
|
||||||
|
$check('status has_token', !empty($data['has_token']));
|
||||||
|
$check('status has_template', !empty($data['has_template']));
|
||||||
|
$check('status bound_users', ($data['bound_users'] ?? 0) === 7);
|
||||||
|
$check('status 掩码 app_secret', ($data['app_secret'] ?? '') === '***');
|
||||||
|
$check('status allowed_types 非空', !empty($data['allowed_types']));
|
||||||
|
|
||||||
|
// 2) 完整更新写入全部字段。
|
||||||
|
$response = $controller->update_wechat_settings(new WP_REST_Request([
|
||||||
|
'enabled' => true,
|
||||||
|
'app_id' => 'wxNEW',
|
||||||
|
'app_secret' => 'NEWSECRET',
|
||||||
|
'template_id' => 'TPL2',
|
||||||
|
]));
|
||||||
|
$check('更新写入 app_id', ($stored()['app_id'] ?? '') === 'wxNEW');
|
||||||
|
$check('更新写入 app_secret', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
|
||||||
|
$check('更新写入 template_id', ($stored()['template_id'] ?? '') === 'TPL2');
|
||||||
|
$check('更新返回成功消息', str_contains((string) ($response->data['message'] ?? ''), '已保存'));
|
||||||
|
|
||||||
|
// 3) 掩码 *** 保留已存密钥。
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request([
|
||||||
|
'enabled' => true,
|
||||||
|
'app_id' => 'wxNEW',
|
||||||
|
'app_secret' => '***',
|
||||||
|
]));
|
||||||
|
$check('掩码保留原密钥', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
|
||||||
|
|
||||||
|
// 4) 缺省字段沿用当前值,开关可关闭。
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request(['enabled' => false]));
|
||||||
|
$check('缺省字段沿用当前值', ($stored()['app_id'] ?? '') === 'wxNEW' && ($stored()['template_id'] ?? '') === 'TPL2');
|
||||||
|
$check('enabled 可关闭', empty($stored()['enabled']));
|
||||||
|
|
||||||
|
// 5) 输入消毒 + 未知键被白名单丢弃。
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request([
|
||||||
|
'app_id' => '<b>wxXSS</b>',
|
||||||
|
'evil' => 'x',
|
||||||
|
]));
|
||||||
|
$check('app_id 经消毒', ($stored()['app_id'] ?? '') === 'wxXSS');
|
||||||
|
$check('未知键被忽略', !array_key_exists('evil', $stored()));
|
||||||
|
|
||||||
|
// 6) 未配置时空密钥可显式清空。
|
||||||
|
update_option('aksrt_wechat_settings', ['enabled' => true, 'app_id' => 'a', 'app_secret' => '', 'template_id' => '']);
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request(['app_secret' => '']));
|
||||||
|
$check('空密钥可清存', ($stored()['app_secret'] ?? '') === '');
|
||||||
|
|
||||||
|
if ($failures) {
|
||||||
|
echo "WeChat settings harness FAILED\n";
|
||||||
|
foreach ($failures as $failure) {
|
||||||
|
echo ' - ' . $failure . "\n";
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "WeChat settings harness passed\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信模板消息 REST 契约(修复存量审计错误):
|
||||||
|
* 1) /wechat/status 与 /wechat/settings 的路由回调方法在 UserController 中真实存在;
|
||||||
|
* 2) 状态读取委托 WeChatNotify::get_status(),写入委托 Settings::update_wechat_settings();
|
||||||
|
* 3) app_secret 掩码 *** 不覆盖已存密钥,输入经 sanitize_text_field 消毒;
|
||||||
|
* 4) PHP harness 驱动真实控制器验证上述行为;
|
||||||
|
* 5) aksrt_settings 上的微信通知死键(wechat_notify_* 等)已彻底移除,
|
||||||
|
* 活配置面仅剩 aksrt_wechat_settings(/users/wechat-template 自定义页)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const controller = fs.readFileSync(path.join(root, 'src/Admin/UserController.php'), 'utf8');
|
||||||
|
const schemaUsers = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
|
||||||
|
const usersSchemaIndex = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/index.js'), 'utf8');
|
||||||
|
const usersSections = fs.readFileSync(path.join(root, 'assets/admin/src/constants/sections/users.js'), 'utf8');
|
||||||
|
|
||||||
|
// 死双路面:写进 aksrt_settings、全仓无消费方的微信通知键。
|
||||||
|
const DEAD_WECHAT_KEYS = [
|
||||||
|
'wechat_notify_enabled',
|
||||||
|
'wechat_app_id',
|
||||||
|
'wechat_app_secret',
|
||||||
|
'wechat_template_id',
|
||||||
|
'wechat_token',
|
||||||
|
'wechat_encoding_aes_key',
|
||||||
|
'wechat_template_comment',
|
||||||
|
'wechat_template_reply',
|
||||||
|
'wechat_template_system',
|
||||||
|
'wechat_template_order',
|
||||||
|
'wechat_notify_comment',
|
||||||
|
'wechat_notify_reply',
|
||||||
|
'wechat_notify_system',
|
||||||
|
'wechat_notify_order',
|
||||||
|
'wechat_bind_entry',
|
||||||
|
];
|
||||||
|
|
||||||
|
test('路由与回调方法配对存在', () => {
|
||||||
|
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/status'[\s\S]*?'callback'\s*=>\s*\[\$this, 'get_wechat_status'\]/);
|
||||||
|
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/settings'[\s\S]*?'callback'\s*=>\s*\[\$this, 'update_wechat_settings'\]/);
|
||||||
|
assert.match(controller, /public function get_wechat_status\(\\WP_REST_Request \$request\)/);
|
||||||
|
assert.match(controller, /public function update_wechat_settings\(\\WP_REST_Request \$request\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('状态与写入委托既有服务层', () => {
|
||||||
|
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
|
||||||
|
assert.match(controller, /Settings::update_wechat_settings\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('密钥掩码与输入消毒', () => {
|
||||||
|
assert.match(controller, /\$params\['app_secret'\] !== '\*\*\*'/);
|
||||||
|
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_id'\]\)/);
|
||||||
|
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_secret'\]\)/);
|
||||||
|
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['template_id'\]\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('微信设置行为 harness 通过', () => {
|
||||||
|
const harness = path.join(root, 'tests', 'admin', 'wechat-settings-harness.php');
|
||||||
|
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||||
|
assert.match(output, /WeChat settings harness passed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('死双路面(aksrt_settings 微信通知键)已彻底移除', () => {
|
||||||
|
for (const key of DEAD_WECHAT_KEYS) {
|
||||||
|
assert.ok(!schemaUsers.includes(key), `SchemaUsers 残留: ${key}`);
|
||||||
|
assert.ok(!usersSections.includes(key), `users 菜单常量残留: ${key}`);
|
||||||
|
}
|
||||||
|
assert.ok(!usersSchemaIndex.includes('wechatSchema'), 'users schema index 残留 wechatSchema 注册');
|
||||||
|
assert.ok(!usersSections.includes("route: '/users/wechat',"), '用户菜单残留微信通知 schema 页');
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/schema/sections/users/wechat.js')), 'wechat.js 未删除');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('活配置面保持唯一(aksrt_wechat_settings + 自定义页)', () => {
|
||||||
|
assert.match(usersSections, /key: 'users-wechat-template'/);
|
||||||
|
assert.match(usersSections, /route: '\/users\/wechat-template'/);
|
||||||
|
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
|
||||||
|
assert.match(controller, /Settings::update_wechat_settings\(/);
|
||||||
|
});
|
||||||
@@ -180,6 +180,52 @@ test('guest account member VIP and publish contracts share reference header DOM'
|
|||||||
assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
|
assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the top-nav midline chip rule leaves the popover social buttons alone', () => {
|
||||||
|
const userboxCss = read('assets/css/frontend/header-userbox.css');
|
||||||
|
const publicCss = read('assets/css/build/public.css');
|
||||||
|
|
||||||
|
// The account-row chips, the theme toggle and the language toggle all want a
|
||||||
|
// 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. flex-basis outranks width in a
|
||||||
|
// flex container and the bare rule is (0,3,0) against (0,2,0), so an
|
||||||
|
// unscoped selector collapses the buttons to 29px and zeroes their margins.
|
||||||
|
assert.match(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)\s*\{[^}]*flex:\s*0 0 29px;[^}]*margin:\s*0;/s);
|
||||||
|
assert.doesNotMatch(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius\s*\{/);
|
||||||
|
// header-userbox.css is also folded into the public bundle, so the exemption
|
||||||
|
// has to survive the build.
|
||||||
|
assert.match(publicCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('route and feature modules do not restyle the legacy top-nav chips', () => {
|
||||||
|
// Route and feature modules may not restyle the legacy .but colour modifiers
|
||||||
|
// without an enclosing scope. The top-nav chips ride the legacy geometry
|
||||||
|
// verbatim (newadd-btns but <skin> radius and pay-vip but jb-red radius
|
||||||
|
// payvip-icon), so their pill comes from .but.radius at (0,2,0). A later,
|
||||||
|
// equally specific rule that restates border-radius on a bare .but.<colour>
|
||||||
|
// out-ranks it and flattens the pill: user-center/credits.css did exactly that
|
||||||
|
// with .but.jb-blue, so the user-center route rendered a 4px publish chip while
|
||||||
|
// every other route kept the 50px one. Such rules must be scoped to their own
|
||||||
|
// shell, as credits.css now is with .uc-content.
|
||||||
|
const moduleCss = [
|
||||||
|
...listFiles('assets/css/routes', new Set(['.css'])),
|
||||||
|
...listFiles('assets/css/frontend', new Set(['.css'])),
|
||||||
|
].filter((file) => file !== 'assets/css/frontend/header-userbox.css');
|
||||||
|
for (const file of moduleCss) {
|
||||||
|
const css = read(file);
|
||||||
|
for (const [, selector, declarations] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
||||||
|
if (!/border-radius/.test(declarations)) continue;
|
||||||
|
for (const part of selector.split(',')) {
|
||||||
|
assert.doesNotMatch(
|
||||||
|
part.trim(),
|
||||||
|
/^\.but\./,
|
||||||
|
`${file} restates border-radius on an unscoped legacy .but modifier (${part.trim()}); scope it to the module's own shell so the top-nav chips keep the reference pill`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('the account popover rows match the reference user box structure', () => {
|
test('the account popover rows match the reference user box structure', () => {
|
||||||
const identity = read('inc/helpers/aksrt-identity.php');
|
const identity = read('inc/helpers/aksrt-identity.php');
|
||||||
const legacyMainCss = read('assets/css/main.css');
|
const legacyMainCss = read('assets/css/main.css');
|
||||||
@@ -200,13 +246,18 @@ test('the account popover rows match the reference user box structure', () => {
|
|||||||
assert.match(header, /admin\.php\?page=aksrt_pay/);
|
assert.match(header, /admin\.php\?page=aksrt_pay/);
|
||||||
assert.match(header, /支付中心/);
|
assert.match(header, /支付中心/);
|
||||||
|
|
||||||
// Wallet tiles: balance first, muted-box geometry, FA5 icons.
|
// Wallet tiles: balance first, muted-box geometry, reference sprite icons.
|
||||||
assert.match(header, /class="flex ab jsb col-ml6"/);
|
assert.match(header, /class="flex ab jsb col-ml6"/);
|
||||||
assert.match(header, /muted-box padding-6 flex1 radius4/);
|
assert.match(header, /muted-box padding-6 flex1 radius4/);
|
||||||
assert.match(header, /font-bold c-blue-2/);
|
assert.match(header, /font-bold c-blue-2/);
|
||||||
assert.match(header, /font-bold c-yellow/);
|
assert.match(header, /font-bold c-yellow/);
|
||||||
assert.match(header, /fa fas fa-money-bill-wave em14 c-blue-2/);
|
assert.match(header, /aksrt_icon_markup\('zibsvg-money-color-2', 'em14'\)/);
|
||||||
assert.match(header, /fa fas fa-coins em14 c-yellow/);
|
assert.match(header, /aksrt_icon_markup\('zibsvg-points-color', 'em14'\)/);
|
||||||
|
// The value icons are the reference theme's multi-tone SVGs, not FA5 glyphs;
|
||||||
|
// the tile arrow stays on fa-angle-right exactly as the reference renders it.
|
||||||
|
assert.doesNotMatch(header, /fa fas fa-money-bill-wave/);
|
||||||
|
assert.doesNotMatch(header, /fa fas fa-coins/);
|
||||||
|
assert.match(header, /fa fas fa-angle-right em12/);
|
||||||
|
|
||||||
// Message icon uses the reference markup.
|
// Message icon uses the reference markup.
|
||||||
assert.match(header, /msg-news-icon abs-right/);
|
assert.match(header, /msg-news-icon abs-right/);
|
||||||
@@ -217,7 +268,12 @@ test('the account popover rows match the reference user box structure', () => {
|
|||||||
assert.match(header, /radius4 payvip-icon btn-block text-center/);
|
assert.match(header, /radius4 payvip-icon btn-block text-center/);
|
||||||
assert.match(header, /vip-expdate-tag/);
|
assert.match(header, /vip-expdate-tag/);
|
||||||
assert.match(header, /pay-vip but radius4 payvip-icon btn-block/);
|
assert.match(header, /pay-vip but radius4 payvip-icon btn-block/);
|
||||||
assert.match(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;/s);
|
// core/buttons.css restates .but after main.css, which would otherwise replace
|
||||||
|
// the legacy .payvip-icon surface with the .jb-* skin. The restoration must
|
||||||
|
// cover background, color, border and text-shadow, and must not be scoped to
|
||||||
|
// one call site or the top-nav chip keeps the wrong gradient.
|
||||||
|
assert.match(userboxCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;[\s\S]*?border:\s*none;[\s\S]*?text-shadow:\s*none;/s);
|
||||||
|
assert.doesNotMatch(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon/)
|
||||||
assert.doesNotMatch(header, /vipbg-v2/);
|
assert.doesNotMatch(header, /vipbg-v2/);
|
||||||
|
|
||||||
// Check-in entry is an anchor with the reference button classes.
|
// Check-in entry is an anchor with the reference button classes.
|
||||||
@@ -225,8 +281,11 @@ test('the account popover rows match the reference user box structure', () => {
|
|||||||
assert.match(header, /but block c-yellow is-done/);
|
assert.match(header, /but block c-yellow is-done/);
|
||||||
assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/);
|
assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/);
|
||||||
assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/);
|
assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/);
|
||||||
assert.match(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s);
|
// The reference selector is a descendant, so the VIP chip inside its hover-show
|
||||||
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{/);
|
// wrapper receives the same .but geometry (font-size, padding, margin) as the
|
||||||
|
// publish chip instead of falling back to the bare .but defaults.
|
||||||
|
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s);
|
||||||
|
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{/);
|
||||||
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
|
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
|
||||||
assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/);
|
assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/);
|
||||||
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
|
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
|
||||||
|
|||||||
@@ -239,3 +239,77 @@ test('OAuth frontend is accessible, redirect-aware, and removes retired provider
|
|||||||
assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i);
|
assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i);
|
||||||
assert.match(mainCss, /\.pay-box \.but\.baidu/);
|
assert.match(mainCss, /\.pay-box \.but\.baidu/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the social-login area drops the implicit agreement line', () => {
|
||||||
|
const shared = read('templates/parts/auth-content.php');
|
||||||
|
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
|
||||||
|
|
||||||
|
// The reference shows no agreement copy under the social buttons; only the
|
||||||
|
// registration panel keeps its own, and it stays reachable.
|
||||||
|
assert.doesNotMatch(shared, /使用社交账号登录即表示/);
|
||||||
|
assert.match(shared, /注册即表示/);
|
||||||
|
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
|
||||||
|
assert.doesNotMatch(userRenderer, /使用社交账号登录即表示|我已阅读并同意/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
|
||||||
|
const icons = read('inc/helpers/aksrt-icons.php');
|
||||||
|
const sprites = read('assets/js/runtime/svg-icon.js');
|
||||||
|
|
||||||
|
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so both
|
||||||
|
// resolve to the theme's own symbol sheet exactly like the reference does.
|
||||||
|
assert.match(icons, /'gitee'\s*=>\s*'zibsvg-gitee'/);
|
||||||
|
assert.match(icons, /'google'\s*=>\s*'zibsvg-google-color'/);
|
||||||
|
assert.match(icons, /'github'\s*=>\s*'fa fab fa-github'/);
|
||||||
|
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/);
|
||||||
|
assert.match(sprites, /aksrt_svgs\['gitee'\]/);
|
||||||
|
assert.match(sprites, /aksrt_svgs\['google-color'\]/);
|
||||||
|
// Sprite values must reach the shared icon renderer instead of being wrapped
|
||||||
|
// in a bogus <i class="zibsvg-…">.
|
||||||
|
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?aksrt_icon_markup\(\$class, \$extra_classes\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the top-nav user popover reproduces the reference quick-login block', () => {
|
||||||
|
const oauth = read('src/Auth/OAuth.php');
|
||||||
|
const header = read('inc/helpers/header/aksrt-header-navbar.php');
|
||||||
|
const links = read('inc/helpers/aksrt-links.php');
|
||||||
|
const legacyCss = read('assets/css/frontend/pages/base-1.css');
|
||||||
|
|
||||||
|
assert.match(oauth, /social-separator separator muted-3-color em09 mt10/);
|
||||||
|
assert.match(oauth, /'<div class="social_loginbar">'/);
|
||||||
|
assert.match(oauth, /'social-login-item ' \. \$type/);
|
||||||
|
assert.match(oauth, /' button-lg' : ' toggle-radius'/);
|
||||||
|
assert.match(oauth, /rel="nofollow"/);
|
||||||
|
assert.match(oauth, /qrcode-signin/);
|
||||||
|
assert.match(oauth, /oauth_button_lg/);
|
||||||
|
|
||||||
|
// Callers append the block verbatim — no hand-rolled separator or wrapper.
|
||||||
|
assert.match(header, /render_social_buttons\(__\('快速登录', 'aksrt'\)\)/);
|
||||||
|
assert.doesNotMatch(header, /social-separator|social_loginbar/);
|
||||||
|
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'aksrt'\)\)/);
|
||||||
|
assert.doesNotMatch(links, /social_loginbar/);
|
||||||
|
|
||||||
|
// The retired .social-login-divider / .oauth-row / .oauth-btn markup and its
|
||||||
|
// stylesheet are gone.
|
||||||
|
assert.doesNotMatch(oauth, /oauth-row|oauth-btn|social-login-divider/);
|
||||||
|
assert.doesNotMatch(legacyCss, /\.oauth-row|\.oauth-btn|\.social-login-divider/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('provider URLs stay raw in the data layer and are escaped once per surface', () => {
|
||||||
|
const oauth = read('src/Auth/OAuth.php');
|
||||||
|
const shared = read('templates/parts/auth-content.php');
|
||||||
|
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
|
||||||
|
const comments = read('inc/helpers/aksrt-comments.php');
|
||||||
|
|
||||||
|
// get_auth_providers_data() hands out raw URLs, exactly like its sibling
|
||||||
|
// get_frontend_bindings_data() does. esc_url() is idempotent on its own
|
||||||
|
// output, so escaping here as well was redundant rather than broken.
|
||||||
|
assert.doesNotMatch(oauth, /'url'\s*=>\s*esc_url\(\$url\)/);
|
||||||
|
assert.match(oauth, /'url'\s*=>\s*\$url,/);
|
||||||
|
|
||||||
|
// Every consumer still escapes on output, so the rendered href is unchanged.
|
||||||
|
assert.match(shared, /esc_url\(\(string\) \(\$provider\['url'\] \?\? '#'\)\)/);
|
||||||
|
assert.match(userRenderer, /href="' \. esc_url\(\$url\)/);
|
||||||
|
assert.match(oauth, /'<a rel="nofollow" href="' \. esc_url\(\$url\)/);
|
||||||
|
assert.match(comments, /<a href="<\?php echo esc_url\(\$url\); \?\>" class="but hollow social-login-item ml10">/);
|
||||||
|
});
|
||||||
@@ -120,6 +120,10 @@ test('defaults, nested structures, dependencies, and excluded product families a
|
|||||||
}
|
}
|
||||||
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
|
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
|
||||||
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
|
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
|
||||||
|
// FA5 spelling of the reference '<i class="fa fa-fw fa-pencil"></i>发布'
|
||||||
|
// default: fa-pencil does not exist in the bundled FA 5.15.4 free set.
|
||||||
|
assert.match(schema, /'nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"><\/i>发布'/)
|
||||||
|
assert.doesNotMatch(schema, /fa fas fa-fw fas /)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('VIP navigation purchase switches are saved and consumed end to end', () => {
|
test('VIP navigation purchase switches are saved and consumed end to end', () => {
|
||||||
@@ -136,8 +140,18 @@ test('VIP navigation purchase switches are saved and consumed end to end', () =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('header VIP hover cards match the legacy markup and style contract', () => {
|
test('header VIP hover cards match the legacy markup and style contract', () => {
|
||||||
assert.match(headerMenuButtons, /<span class="hover-show inline-block navbar-vip-hover">/)
|
assert.match(headerMenuButtons, /<span class="hover-show inline-block">/)
|
||||||
assert.match(headerMenuButtons, /<div class="hover-show-con sub-menu sub-vip-card">/)
|
assert.match(headerMenuButtons, /<div class="sub-menu hover-show-con sub-vip-card">/)
|
||||||
|
assert.match(headerMenuButtons, /'<a class="' \. esc_attr\(\$cls\) \. ' but jb-red radius payvip-icon ml10" href="javascript:;">'/)
|
||||||
|
// The payvip surface must survive the .but compatibility layer for the top-nav
|
||||||
|
// chip too, not just the user-card CTA.
|
||||||
|
assert.match(headerCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?--this-color:\s*#ece0e0;/s)
|
||||||
|
assert.match(headerMenuButtons, /<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
|
||||||
|
assert.match(headerMenuButtons, /'<a rel="nofollow" class="newadd-btns ' \. esc_attr\(\$class\) \. ' btn-newadd"/)
|
||||||
|
assert.equal((headerMenuButtons.match(/Settings::close_sign\(\)/g) || []).length, 2)
|
||||||
|
for (const dead of ['navbar-cta', 'navbar-cta-text', 'navbar-newposts', 'navbar-vip-hover', 'newadd-btns__', 'vip-level', 'fa-gem']) {
|
||||||
|
assert.equal(headerMenuButtons.includes(dead), false, `AKSRT-only header CTA token remains: ${dead}`)
|
||||||
|
}
|
||||||
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
|
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
|
||||||
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
|
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
|
||||||
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
|
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
|
||||||
@@ -154,8 +168,21 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
|
|||||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
|
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
|
||||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
|
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
|
||||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
|
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover\s*\{[^}]*align-self:\s*stretch;[^}]*overflow:\s*visible;/s)
|
// The hover wrapper must stay a plain block container so the legacy static
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover > \.sub-vip-card\s*\{[^}]*top:\s*100%;[^}]*right:\s*0;[^}]*left:\s*auto;[^}]*max-width:\s*calc\(100vw - 24px\);/s)
|
// position of .sub-vip-card matches the reference theme.
|
||||||
|
assert.match(headerCss, /\.navbar-top \.navbar-but > \.hover-show\s*\{[^}]*display:\s*inline-block;/s)
|
||||||
|
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
|
||||||
|
assert.match(legacyMainCss, /\.navbar-top \.navbar-right \.sub-menu\s*\{[^}]*right:\s*0;/s)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('user card VIP CTA matches the reference zib_get_header_payvip_icon()', () => {
|
||||||
|
assert.match(headerUserbox, /'<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'/)
|
||||||
|
assert.match(headerUserbox, /<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
|
||||||
|
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'aksrt'\)/)
|
||||||
|
for (const dead of ['fa-gem', 'vip-level']) {
|
||||||
|
assert.equal(headerUserbox.includes(dead), false, `AKSRT-only user card CTA token remains: ${dead}`)
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(headerUserbox, /<span>' \. esc_html__\('开通会员 尊享会员权益'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
|
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
|
||||||
@@ -322,13 +349,30 @@ test('user profile and author widgets render the reference user-card contract',
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('desktop header actions share one row without generic button masks', () => {
|
test('desktop header actions share one row without generic button masks', () => {
|
||||||
assert.match(headerNavbar, /aksrt-navbar-actions navbar-form navbar-right/)
|
assert.match(headerNavbar, /navbar-form navbar-right navbar-but/)
|
||||||
|
assert.match(headerNavbar, /navbar-form navbar-right flex ac/)
|
||||||
|
assert.match(headerNavbar, /\$fold \. \$button \. \$right_but/)
|
||||||
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
|
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
|
||||||
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
|
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;/s)
|
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*display:\s*inline-flex;[^}]*align-items:\s*center;/s)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
|
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta\.radius\s*\{[^}]*border-radius:\s*50px;/s)
|
// Bootstrap's base .navbar-form rule keeps margin-top: 8px at >=768px (the
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta > \.icon\s*\{[^}]*width:\s*1\.2em;[^}]*height:\s*1\.2em;[^}]*flex:\s*0 0 auto;/s)
|
// override only zeroes the horizontal margins), so every right-cluster group
|
||||||
|
// starts 8px below the top of .navbar-collapse. A 34px cross-size therefore
|
||||||
|
// puts each group's midline on the navbar's 25px line, where the 50px menu row
|
||||||
|
// and the logo already sit. A 50px min-height double-counts that 8px: the
|
||||||
|
// group's content drops 8px below the line and the navbar grows from 50px to
|
||||||
|
// 58px, which also breaks the body.nav-fixed offset.
|
||||||
|
assert.doesNotMatch(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*min-height/s)
|
||||||
|
assert.match(headerCss, /\.navbar-top \.navbar-form\.flex\.ac\s*\{[^}]*min-height:\s*34px;/s)
|
||||||
|
// The publish and VIP chips ride the legacy .but geometry verbatim; nothing in
|
||||||
|
// the header module may restate their padding, margin, radius or icon size.
|
||||||
|
// Descendant, not child: the VIP chip lives inside the hover-show wrapper.
|
||||||
|
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s)
|
||||||
|
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
|
||||||
|
// No merged flex container: the three groups keep their own .navbar-form
|
||||||
|
// padding, which is what produces the reference inter-cluster spacing.
|
||||||
|
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions/)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
|
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
|
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AksField 的 group / repeater 行身份稳定性契约。
|
||||||
|
*
|
||||||
|
* 背景:inline 展开模式用 vuedraggable 渲染行,vuedraggable 的 computeNodes 会把
|
||||||
|
* `node.key = getKey(element)`,而行 key 由 AksField 的 repeaterDragKey 提供。服务端
|
||||||
|
* 历史行没有 _id,key 来自以「对象身份」为键的 WeakMap。
|
||||||
|
*
|
||||||
|
* 关键坑:行数据是 reactive 的。updateRepeaterItem 一旦把行展开成一个新对象,渲染时
|
||||||
|
* 读到的是那个新对象的又一个 reactive 代理——身份再次改变,WeakMap 认不出它便生成
|
||||||
|
* 新 key,整行随之重挂载、输入框失焦,表现为「打一个字就退出编辑」。所以必须原地写,
|
||||||
|
* 让行身份跨渲染保持不变。本测试直接跑这两个真实函数来锁定该不变量。
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 fieldVue = fs.readFileSync(
|
||||||
|
path.join(root, 'assets', 'admin', 'src', 'components', 'form', 'AksField.vue'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟 Vue 的 reactive():每个原始对象只包一次代理并缓存,之后读多少次都返回同一个
|
||||||
|
* 代理。这正是让 WeakMap 身份在渲染与更新之间保持一致(或不一致)的原因。
|
||||||
|
*/
|
||||||
|
const proxyCache = new WeakMap();
|
||||||
|
const proxySet = new WeakSet();
|
||||||
|
function reactiveOf(value) {
|
||||||
|
if (!value || typeof value !== 'object' || proxySet.has(value)) return value;
|
||||||
|
let proxy = proxyCache.get(value);
|
||||||
|
if (!proxy) {
|
||||||
|
proxy = new Proxy(value, {
|
||||||
|
get(target, key, receiver) { return Reflect.get(target, key, receiver); },
|
||||||
|
set(target, key, next) { target[key] = next; return true; },
|
||||||
|
});
|
||||||
|
proxyCache.set(value, proxy);
|
||||||
|
proxySet.add(proxy);
|
||||||
|
}
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取一整行声明(WeakMap / 种子计数器都没有花括号体)。 */
|
||||||
|
function declaration(source, marker) {
|
||||||
|
const start = source.indexOf(marker);
|
||||||
|
assert.notStrictEqual(start, -1, `AksField.vue 缺少声明: ${marker}`);
|
||||||
|
const end = source.indexOf('\n', start);
|
||||||
|
return source.slice(start, end === -1 ? undefined : end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按花括号配平切出一个顶层函数(这些函数体内没有字符串/注释里的孤立花括号)。 */
|
||||||
|
function functionBlock(source, marker) {
|
||||||
|
const start = source.indexOf(marker);
|
||||||
|
assert.notStrictEqual(start, -1, `AksField.vue 缺少函数: ${marker}`);
|
||||||
|
const open = source.indexOf('{', start);
|
||||||
|
assert.notStrictEqual(open, -1, `函数没有函数体: ${marker}`);
|
||||||
|
let depth = 0;
|
||||||
|
for (let i = open; i < source.length; i++) {
|
||||||
|
if (source[i] === '{') depth++;
|
||||||
|
else if (source[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) return source.slice(start, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.fail(`花括号不配平: ${marker}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prelude = [
|
||||||
|
declaration(fieldVue, 'const dragRowKeys = new WeakMap()'),
|
||||||
|
declaration(fieldVue, 'let dragRowKeySeed = 0'),
|
||||||
|
functionBlock(fieldVue, 'function repeaterDragKey(item)'),
|
||||||
|
functionBlock(fieldVue, 'function updateRepeaterItem(index, fieldKey, value)'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
/** 用真实的 props / emit 形状驱动这两个函数。 */
|
||||||
|
function makeField(rows) {
|
||||||
|
const emitted = [];
|
||||||
|
// props.modelValue 是 reactive 视图,正如真实组件收到的那样。
|
||||||
|
const props = { modelValue: rows.map(reactiveOf) };
|
||||||
|
const emit = (event, value) => emitted.push({ event, value });
|
||||||
|
const factory = new Function('props', 'emit', `${prelude}\nreturn { repeaterDragKey, updateRepeaterItem };`);
|
||||||
|
const api = factory(props, emit);
|
||||||
|
// 父组件把 emit 出来的数组写回 reactive 树,读出来时行又被包成代理。
|
||||||
|
const roundTrip = () => {
|
||||||
|
props.modelValue = emitted[emitted.length - 1].value.map(reactiveOf);
|
||||||
|
};
|
||||||
|
return { api, props, emitted, roundTrip };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('连续输入期间行身份不变,drag key 保持稳定', () => {
|
||||||
|
// 服务端历史行:没有 _id,key 只能由 WeakMap 派生。
|
||||||
|
const serverRows = [
|
||||||
|
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||||
|
{ points: 5000, level: 1, time: 'Permanent' },
|
||||||
|
];
|
||||||
|
const { api, props, emitted, roundTrip } = makeField(serverRows);
|
||||||
|
const firstKey = api.repeaterDragKey(props.modelValue[0]);
|
||||||
|
const secondKey = api.repeaterDragKey(props.modelValue[1]);
|
||||||
|
assert.notStrictEqual(firstKey, secondKey, '两行必须各有一个 key');
|
||||||
|
const firstRow = props.modelValue[0];
|
||||||
|
|
||||||
|
// 模拟逐字输入:每次 emit 后父组件把新数组回填 props.modelValue。
|
||||||
|
let rows = serverRows;
|
||||||
|
for (const value of ['5', '50', '500']) {
|
||||||
|
api.updateRepeaterItem(0, 'points', value);
|
||||||
|
rows = emitted[emitted.length - 1].value;
|
||||||
|
roundTrip();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(rows[0].points, '500', '输入值应写进行内');
|
||||||
|
// 核心不变量:行对象身份必须跨渲染保持不变,否则 WeakMap 会发新 key。
|
||||||
|
assert.strictEqual(props.modelValue[0], firstRow, '行身份必须在编辑期间保持不变');
|
||||||
|
assert.strictEqual(
|
||||||
|
api.repeaterDragKey(props.modelValue[0]),
|
||||||
|
firstKey,
|
||||||
|
'行 key 必须在编辑期间保持稳定,否则整行重挂载、输入框失焦'
|
||||||
|
);
|
||||||
|
assert.strictEqual(api.repeaterDragKey(props.modelValue[1]), secondKey, '未编辑的行 key 不应变');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emit 出去的是新数组,父组件仍会重渲染', () => {
|
||||||
|
const { api, props, emitted, roundTrip } = makeField([{ points: 500 }]);
|
||||||
|
const before = props.modelValue;
|
||||||
|
api.updateRepeaterItem(0, 'points', '9');
|
||||||
|
const after = emitted[emitted.length - 1].value;
|
||||||
|
assert.notStrictEqual(after, before, '必须发出新数组,否则父组件不会重渲染');
|
||||||
|
roundTrip();
|
||||||
|
assert.strictEqual(props.modelValue[0].points, '9');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('行不是对象时退化为新建,不抛错', () => {
|
||||||
|
const { api, props, roundTrip } = makeField([]);
|
||||||
|
api.updateRepeaterItem(0, 'points', '7');
|
||||||
|
roundTrip();
|
||||||
|
assert.strictEqual(props.modelValue.length, 1);
|
||||||
|
assert.strictEqual(props.modelValue[0].points, '7');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('带 _id 的行仍以 _id 作为 key', () => {
|
||||||
|
const rows = [{ _id: 'rpt_1', points: 100 }, { _id: 'rpt_2', points: 200 }];
|
||||||
|
const { api, props } = makeField(rows);
|
||||||
|
assert.strictEqual(api.repeaterDragKey(props.modelValue[0]), 'rpt_1');
|
||||||
|
assert.strictEqual(api.repeaterDragKey(props.modelValue[1]), 'rpt_2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('新增行获得独立 key,不与既有行冲突', () => {
|
||||||
|
const { api, props } = makeField([{ points: 500 }, { points: 5000 }]);
|
||||||
|
const keys = new Set(props.modelValue.map((row) => api.repeaterDragKey(row)));
|
||||||
|
assert.strictEqual(keys.size, 2);
|
||||||
|
assert.ok(!keys.has(api.repeaterDragKey({ points: 0 })), '新行必须拿到全新 key');
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
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 = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||||
|
|
||||||
|
// Reference theme (zibll css/main.css): .jb-yellow sets --this-bg-b: #f59f54 and
|
||||||
|
// --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%). build/public.min.css
|
||||||
|
// is enqueued after main.min.css, so the legacy .jb-* compat layer in core/buttons.css
|
||||||
|
// overrides main.css there; the two must agree or every .jb-yellow surface (the
|
||||||
|
// user-card check-in badge first of all) paints the wrong gradient.
|
||||||
|
const referenceGradient = /--this-bg:\s*linear-gradient\(135deg,\s*#f59f54 10%,\s*#ff6922 100%\)/
|
||||||
|
const staleHexes = /#f99d4d|#f7631d/
|
||||||
|
|
||||||
|
test('buttons.css legacy .jb-yellow gradient matches the reference theme', () => {
|
||||||
|
const buttons = read('assets/css/core/buttons.css')
|
||||||
|
const start = buttons.indexOf('.jb-yellow {')
|
||||||
|
assert.notEqual(start, -1, 'missing the legacy .jb-yellow rule')
|
||||||
|
const block = buttons.slice(start, buttons.indexOf('}', start))
|
||||||
|
assert.match(block, referenceGradient)
|
||||||
|
assert.doesNotMatch(block, staleHexes)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the built public bundle carries the reference .jb-yellow gradient', () => {
|
||||||
|
const bundle = read('assets/css/build/public.min.css')
|
||||||
|
assert.match(
|
||||||
|
bundle,
|
||||||
|
/\.jb-yellow\{--this-bg: linear-gradient\(135deg, #f59f54 10%, #ff6922 100%\)\}/
|
||||||
|
)
|
||||||
|
assert.doesNotMatch(
|
||||||
|
bundle,
|
||||||
|
/\.jb-yellow\{--this-bg: linear-gradient\(135deg, #f99d4d 10%, #f7631d 100%\)\}/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('main.css still owns the reference .jb-yellow gradient', () => {
|
||||||
|
const main = read('assets/css/main.css')
|
||||||
|
assert.match(main, referenceGradient)
|
||||||
|
assert.doesNotMatch(main, staleHexes)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the badge typography is pinned to the reference bootstrap body values', () => {
|
||||||
|
const main = read('assets/css/main.css')
|
||||||
|
const start = main.indexOf('.user-card .card-content .img-badge')
|
||||||
|
assert.notEqual(start, -1, 'missing the user-card img-badge adaptation rule')
|
||||||
|
const block = main.slice(start, main.indexOf('}', start))
|
||||||
|
assert.match(block, /line-height:\s*1\.42857143/)
|
||||||
|
// Reference body font stack (zibll bootstrap.min.css; zibll main.css does not
|
||||||
|
// override it), while AKSRT's reset imposes the modern --font-body stack.
|
||||||
|
assert.match(block, /font-family:\s*"Helvetica Neue", Helvetica, Arial, sans-serif/)
|
||||||
|
// The AKSRT html reset forces antialiased + optimizeLegibility; the badge
|
||||||
|
// restores the browser defaults the reference renders with.
|
||||||
|
assert.match(block, /-webkit-font-smoothing:\s*auto/)
|
||||||
|
assert.match(block, /text-rendering:\s*auto/)
|
||||||
|
|
||||||
|
const min = read('assets/css/main.min.css')
|
||||||
|
assert.match(
|
||||||
|
min,
|
||||||
|
/\.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\}/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the sidebar widget link color does not recolor the check-in badge', () => {
|
||||||
|
// .widget a (sidebar.css, shipped in the homepage bundle) out-specifies the
|
||||||
|
// .jb-* color group, so the badge would paint --color-ink-2 instead of the
|
||||||
|
// white --this-color; the established .but restoration must cover img-badge.
|
||||||
|
const sidebar = read('assets/css/frontend/sidebar.css')
|
||||||
|
const start = sidebar.indexOf('.widget a.but')
|
||||||
|
assert.notEqual(start, -1, 'missing the .widget a color restoration')
|
||||||
|
const block = sidebar.slice(start, sidebar.indexOf('}', start))
|
||||||
|
assert.match(block, /\.widget a\.but,/)
|
||||||
|
assert.match(block, /\.widget a\.img-badge/)
|
||||||
|
assert.match(block, /color:\s*var\(--this-color/)
|
||||||
|
|
||||||
|
const bundle = read('assets/css/build/homepage.min.css')
|
||||||
|
assert.match(bundle, /\.widget a\.but,\.widget a\.img-badge\{color:var\(--this-color, #fff\)\}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the user-card check-in badge renders without the dead is-done class and bare span', () => {
|
||||||
|
const userbox = read('inc/helpers/aksrt-header-userbox.php')
|
||||||
|
const fnStart = userbox.indexOf('function aksrt_header_user_checkin_btn')
|
||||||
|
assert.notEqual(fnStart, -1, 'missing aksrt_header_user_checkin_btn')
|
||||||
|
const fnEnd = userbox.indexOf('if (!function_exists', fnStart)
|
||||||
|
assert.notEqual(fnEnd, -1)
|
||||||
|
const fn = userbox.slice(fnStart, fnEnd)
|
||||||
|
|
||||||
|
// Signed card badge: the badge class passes through untouched and the done
|
||||||
|
// label is bare text after the icon, matching the reference markup.
|
||||||
|
assert.match(fn, /aksrt_get_user_checkin_details_link\(\s*\$class,/)
|
||||||
|
assert.doesNotMatch(fn, /\$class \. ' is-done'/)
|
||||||
|
assert.doesNotMatch(fn, /'<span>'/)
|
||||||
|
|
||||||
|
// The idle badge keeps the data-signin-label hook the sign-in controller
|
||||||
|
// uses to swap labels in place.
|
||||||
|
assert.match(fn, /<span data-signin-label>/)
|
||||||
|
})
|
||||||
@@ -23,9 +23,13 @@ test('user and author widgets render the reference state contract', () => {
|
|||||||
assert.match(data.guest, /signup-loader ml10 but jb-yellow padding-lg/)
|
assert.match(data.guest, /signup-loader ml10 but jb-yellow padding-lg/)
|
||||||
assert.match(data.guest, /social-separator separator muted-3-color em09 mt20 mb20/)
|
assert.match(data.guest, /social-separator separator muted-3-color em09 mt20 mb20/)
|
||||||
assert.match(data.guest, /social_loginbar/)
|
assert.match(data.guest, /social_loginbar/)
|
||||||
assert.match(data.guest, /class="social-login-item github"/)
|
assert.match(data.guest, /class="social-login-item github toggle-radius"/)
|
||||||
assert.match(data.guest, /class="social-login-item qq" data-provider="clogin_qq"/)
|
assert.match(data.guest, /class="social-login-item qq toggle-radius" data-provider="clogin_qq"/)
|
||||||
|
assert.match(data.guest, /title="GitHub登录"/)
|
||||||
assert.doesNotMatch(data.guest, /aksrt-widget|aksrt-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/)
|
assert.doesNotMatch(data.guest, /aksrt-widget|aksrt-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/)
|
||||||
|
// The social-login area carries no agreement copy; the reference guest card
|
||||||
|
// only shows the separator, the .social_loginbar and the provider buttons.
|
||||||
|
assert.doesNotMatch(data.guest, /我已阅读并同意|使用社交账号登录即表示|注册即表示/)
|
||||||
|
|
||||||
assert.match(data.guest_no_registration, /signin-loader but jb-blue padding-lg/)
|
assert.match(data.guest_no_registration, /signin-loader but jb-blue padding-lg/)
|
||||||
assert.doesNotMatch(data.guest_no_registration, /signup-loader/)
|
assert.doesNotMatch(data.guest_no_registration, /signup-loader/)
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* 用户中心左侧栏图标契约(参考主题 1:1 对齐)。
|
||||||
|
*
|
||||||
|
* 硬约束:
|
||||||
|
* 1) 「我的服务 / 功能设置」两个网格使用参考主题的 icon-but-box / user-icon-but-box
|
||||||
|
* 外壳,图标全部来自主题内置 SVG sprite(aksrt_icon_markup('zibsvg-*')),
|
||||||
|
* 不再出现 Font Awesome 字形。
|
||||||
|
* 2) 侧栏其余部件(余额/积分卡、创作/推广卡、VIP 入口、抽屉触发器)同样只使用
|
||||||
|
* sprite 图标,箭头统一为 zibsvg-right,保证侧栏只剩一种图标族。
|
||||||
|
* 3) 旧的 uc-service-grid 外壳、色调底色与计数角标必须已彻底移除,不得残留。
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
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 = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||||
|
|
||||||
|
const sidebarDir = 'inc/helpers/user-center-renderers/';
|
||||||
|
const sidebarFiles = [
|
||||||
|
'sidebar-nav.php',
|
||||||
|
'sidebar-commerce.php',
|
||||||
|
'sidebar-creator.php',
|
||||||
|
'sidebar-vip.php',
|
||||||
|
'profile-sidebar.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
test('服务/设置网格使用参考主题 icon-but-box 外壳与 sprite 图标', () => {
|
||||||
|
const nav = read(sidebarDir + 'sidebar-nav.php');
|
||||||
|
|
||||||
|
assert.match(nav, /class="flex ac hh text-center icon-but-box user-icon-but-box"/);
|
||||||
|
assert.match(nav, /<div class="em16"><\?php echo \$icon_html;/);
|
||||||
|
assert.match(nav, /<div class="px12 muted-color mt3">/);
|
||||||
|
assert.match(nav, /aksrt_icon_markup\('zibsvg-' \. \(string\) \$item\['icon'\]\)/);
|
||||||
|
|
||||||
|
// 参考主题选型:我的服务 = trend-color / user-auth / order-color。
|
||||||
|
assert.match(nav, /'icon'\s+=>\s+'trend-color'/);
|
||||||
|
assert.match(nav, /'icon'\s+=>\s+'user-auth'/);
|
||||||
|
assert.match(nav, /'icon'\s+=>\s+'order-color'/);
|
||||||
|
// 功能设置 = msg-color / user-color / security-color。
|
||||||
|
assert.match(nav, /'icon'\s+=>\s+'msg-color'/);
|
||||||
|
assert.match(nav, /'icon'\s+=>\s+'user-color'/);
|
||||||
|
assert.match(nav, /'icon'\s+=>\s+'security-color'/);
|
||||||
|
|
||||||
|
// 每项都带 tab,保证 icon-but-* 类名完整且选中态可同步。
|
||||||
|
for (const tab of ['level', 'auth', 'orders', 'msg', 'profile', 'settings']) {
|
||||||
|
assert.match(nav, new RegExp(`'tab'\\s+=>\\s+'${tab}'`), `missing tab key: ${tab}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AKSRT 的跳转契约保留 <a href> + data-tab,不搬参考主题的 <item data-onclick>。
|
||||||
|
assert.match(nav, /<a class="<\?php echo esc_attr\(\$item_class\);/);
|
||||||
|
assert.match(nav, /data-tab="<\?php echo esc_attr\(\$tab\); \?>/);
|
||||||
|
assert.doesNotMatch(nav, /data-onclick/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('侧栏五个部件均不再出现 Font Awesome 字形', () => {
|
||||||
|
for (const file of sidebarFiles) {
|
||||||
|
const source = read(sidebarDir + file);
|
||||||
|
assert.doesNotMatch(source, /fa\s+fas\s+fa-/, `${file} still renders a Font Awesome glyph`);
|
||||||
|
assert.doesNotMatch(source, /<i class="fa/, `${file} still renders a Font Awesome <i>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('其余侧栏部件按参考主题选型替换为 sprite 图标', () => {
|
||||||
|
const commerce = read(sidebarDir + 'sidebar-commerce.php');
|
||||||
|
assert.match(commerce, /aksrt_icon_markup\('zibsvg-money-color-2'\)/);
|
||||||
|
assert.match(commerce, /aksrt_icon_markup\('zibsvg-points-color'\)/);
|
||||||
|
assert.match(commerce, /aksrt_icon_markup\('zibsvg-right'\)/);
|
||||||
|
|
||||||
|
const creator = read(sidebarDir + 'sidebar-creator.php');
|
||||||
|
assert.match(creator, /aksrt_icon_markup\('zibsvg-merchant-color'\)/);
|
||||||
|
assert.match(creator, /aksrt_icon_markup\('zibsvg-money-color'\)/);
|
||||||
|
assert.match(creator, /aksrt_icon_markup\('zibsvg-right'\)/);
|
||||||
|
|
||||||
|
const vip = read(sidebarDir + 'sidebar-vip.php');
|
||||||
|
assert.match(vip, /aksrt_icon_markup\('zibsvg-vip_1'\)/);
|
||||||
|
assert.match(vip, /aksrt_icon_markup\('zibsvg-right', 'uc-sidebar-vip__arrow'\)/);
|
||||||
|
|
||||||
|
const profile = read(sidebarDir + 'profile-sidebar.php');
|
||||||
|
assert.match(profile, /aksrt_icon_markup\('zibsvg-menu'\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('旧 uc-service-grid 外壳与色调底色已彻底移除', () => {
|
||||||
|
const shell = read('assets/css/routes/user-center/shell.css');
|
||||||
|
|
||||||
|
assert.doesNotMatch(shell, /uc-service-grid/);
|
||||||
|
for (const file of sidebarFiles) {
|
||||||
|
assert.doesNotMatch(read(sidebarDir + file), /uc-service-grid/);
|
||||||
|
}
|
||||||
|
// 计数角标与 tone 键随色调 CSS 一并成为死代码。
|
||||||
|
assert.doesNotMatch(shell, /uc-service-grid__icon small/);
|
||||||
|
assert.doesNotMatch(read(sidebarDir + 'sidebar-nav.php'), /\$item\['count'\]/);
|
||||||
|
assert.doesNotMatch(read(sidebarDir + 'sidebar-nav.php'), /'tone'/);
|
||||||
|
|
||||||
|
// 参考主题网格尺寸落地。
|
||||||
|
assert.match(shell, /\.user-icon-but-box > a \{[^}]*width: calc\(25% - 10px\)/s);
|
||||||
|
assert.match(shell, /\.user-icon-but-box > a \{[^}]*min-width: 50px/s);
|
||||||
|
assert.match(shell, /\.user-icon-but-box > a\.is-active/);
|
||||||
|
|
||||||
|
// 卡片上的 <i> 已换成 <svg>,元素选择器需覆盖两者。
|
||||||
|
assert.match(shell, /\.uc-summary-card__body > svg/);
|
||||||
|
assert.match(shell, /\.uc-center-card > svg/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('所用 sprite key 均在前端 sprite 中定义', () => {
|
||||||
|
const sprite = read('assets/js/runtime/svg-icon.js');
|
||||||
|
const keys = [
|
||||||
|
'trend-color', 'user-auth', 'order-color', 'msg-color', 'user-color', 'security-color',
|
||||||
|
'money-color-2', 'points-color', 'merchant-color', 'money-color', 'vip_1', 'menu', 'right',
|
||||||
|
];
|
||||||
|
for (const key of keys) {
|
||||||
|
assert.match(sprite, new RegExp(`aksrt_svgs\\['${key}'\\]\\s*=`), `sprite key missing: ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('导航栏用户浮层的余额/积分磁贴与侧栏使用同一组 sprite key', () => {
|
||||||
|
const nav = read(sidebarDir + 'sidebar-commerce.php');
|
||||||
|
const wallet = read('inc/helpers/aksrt-header-userbox.php');
|
||||||
|
|
||||||
|
assert.match(wallet, /aksrt_icon_markup\('zibsvg-money-color-2', 'em14'\)/);
|
||||||
|
assert.match(wallet, /aksrt_icon_markup\('zibsvg-points-color', 'em14'\)/);
|
||||||
|
assert.match(nav, /aksrt_icon_markup\('zibsvg-money-color-2'\)/);
|
||||||
|
assert.match(nav, /aksrt_icon_markup\('zibsvg-points-color'\)/);
|
||||||
|
|
||||||
|
// 磁贴箭头沿用参考主题的 fa-angle-right,不做多余替换。
|
||||||
|
assert.match(wallet, /fa fas fa-angle-right em12/);
|
||||||
|
assert.doesNotMatch(wallet, /fa fas fa-money-bill-wave/);
|
||||||
|
assert.doesNotMatch(wallet, /fa fas fa-coins/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* VIP 后台设置弹窗文案字段契约(对照参考主题 zibll 修正)。
|
||||||
|
*
|
||||||
|
* 硬约束:
|
||||||
|
* 1) 弹窗文案字段更名为「会员介绍」(pay_user_vip_desc, input) 与
|
||||||
|
* 「开通会员更多内容」(pay_user_vip_more, textarea rows 4 +
|
||||||
|
* sanitize_type html),并从「会员等级」组移到分区末尾的
|
||||||
|
* 「界面文案」(modal-copy) 独立分组;
|
||||||
|
* 2) 文案默认值对齐 zibll(黄金会员/钻石会员、尊享专属特权、权益四项、
|
||||||
|
* 会员介绍与开通会员更多内容默认值),SPA schema / PHP schema /
|
||||||
|
* PaymentRuntime 回退三处一致——Settings::get_all() 不合并
|
||||||
|
* schema 默认值,运行时回退即实际默认值;
|
||||||
|
* 3) vip_renew_desc / vip_upgrade_desc 为参考主题的一句话简介字段,
|
||||||
|
* 两份 schema 与 PaymentRuntime 均已定义,并由用户中心 /user/vip/
|
||||||
|
* 档位卡渲染消费(vip-tab.php 按续费/升级动作取对应简介);
|
||||||
|
* 4) 权益与卡密说明字段保存时保留 HTML(sanitize_type html),
|
||||||
|
* 不再被 sanitize_textarea_field 剥掉标签。
|
||||||
|
*/
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..', '..');
|
||||||
|
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
|
||||||
|
|
||||||
|
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
|
||||||
|
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
|
||||||
|
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
|
||||||
|
const rechargeJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js');
|
||||||
|
const vipTabPhp = read('inc', 'helpers', 'user-center-renderers', 'vip-tab.php');
|
||||||
|
const modalSource = read('src', 'Content', 'VipServiceModal.php');
|
||||||
|
const payVipJs = read('assets', 'js', 'pay-vip.js');
|
||||||
|
|
||||||
|
const DESC_DEFAULT = '开通VIP会员,享受会员专属折扣以及多项特权';
|
||||||
|
const MORE_DEFAULT = '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>';
|
||||||
|
const EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||||
|
|
||||||
|
// 取两个标记之间的源码段(分组切片,避免断言误伤其它功能的同名键)。
|
||||||
|
function slice(source, from, to) {
|
||||||
|
const start = source.indexOf(from);
|
||||||
|
assert.notStrictEqual(start, -1, `missing ${from}`);
|
||||||
|
const end = source.indexOf(to, start + from.length);
|
||||||
|
return source.slice(start, end === -1 ? undefined : end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为正确类型', () => {
|
||||||
|
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp]]) {
|
||||||
|
assert.ok(!src.includes('弹窗顶部会员说明') && !src.includes('弹窗底部补充说明 HTML'), `${name} 旧名称应已移除`);
|
||||||
|
assert.ok(src.includes('会员介绍') && src.includes('开通会员更多内容'), `${name} 应含新名称`);
|
||||||
|
}
|
||||||
|
// 会员介绍为单行 input(zibll text「一句话简介」);开通会员更多内容为 rows 4 的 HTML textarea。
|
||||||
|
assert.match(
|
||||||
|
vipSchemaJs,
|
||||||
|
/key: 'pay_user_vip_desc',\s*\n\s*label: '会员介绍',\s*\n\s*type: 'input',\s*\n\s*default: '开通VIP会员,享受会员专属折扣以及多项特权'/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
vipSchemaJs,
|
||||||
|
/key: 'pay_user_vip_more',\s*\n\s*label: '开通会员更多内容',\s*\n\s*type: 'textarea',\s*\n\s*sanitize_type: 'html'[\s\S]*?rows: 4/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
schemaPhp,
|
||||||
|
/'key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权'/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
schemaPhp,
|
||||||
|
/'key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html'[\s\S]*?'rows' => 4/
|
||||||
|
);
|
||||||
|
// 帮助文本对齐 zibll 的 desc。
|
||||||
|
assert.ok(vipSchemaJs.includes('显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍'));
|
||||||
|
assert.ok(vipSchemaJs.includes('显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('两个文案字段位于分区末尾的界面文案分组且不再属于会员等级组', () => {
|
||||||
|
const modalJs = vipSchemaJs.indexOf("key: 'modal-copy'");
|
||||||
|
const modalPhp = schemaPhp.indexOf("'key' => 'modal-copy'");
|
||||||
|
assert.notStrictEqual(modalJs, -1, 'vip.js 应含 modal-copy 分组');
|
||||||
|
assert.notStrictEqual(modalPhp, -1, 'SchemaPayment.php 应含 modal-copy 分组');
|
||||||
|
assert.ok(modalJs > vipSchemaJs.indexOf("key: 'points-exchange'"), 'vip.js modal-copy 应在分区末尾');
|
||||||
|
assert.ok(modalPhp > schemaPhp.indexOf("'key' => 'points-exchange'"), 'SchemaPayment.php modal-copy 应在分区末尾');
|
||||||
|
// modal-copy 之后不再出现其它分组/字段 key。
|
||||||
|
// modal-copy 之后(至分区末尾)不再出现其它分组/字段 key;dependsOn 引用的等级开关除外。
|
||||||
|
assert.doesNotMatch(vipSchemaJs.slice(modalJs), /key: '(?!modal-copy|pay_user_vip_desc|pay_user_vip_more|pay_user_vip_1_s|pay_user_vip_2_s)/);
|
||||||
|
const phpTail = slice(schemaPhp, "'key' => 'modal-copy'", "'revenue'");
|
||||||
|
assert.doesNotMatch(phpTail, /'key' => '(?!modal-copy|pay_user_vip_desc|pay_user_vip_more|pay_user_vip_1_s|pay_user_vip_2_s)/);
|
||||||
|
// 「会员等级」组不再包含这两个字段。
|
||||||
|
assert.doesNotMatch(slice(vipSchemaJs, "key: 'basic'", "key: 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/);
|
||||||
|
assert.doesNotMatch(slice(schemaPhp, "'key' => 'basic'", "'key' => 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/);
|
||||||
|
// 「会员等级」组只留导航按钮与两个等级开关;等级身份字段全部搬进
|
||||||
|
// 「会员购买选项」的等级 Tab(名称/简介/权益/图标)。
|
||||||
|
const basicJs = slice(vipSchemaJs, "key: 'basic'", "key: 'benefit'");
|
||||||
|
const basicPhp = slice(schemaPhp, "'key' => 'basic'", "'key' => 'benefit'");
|
||||||
|
for (const key of ['pay_user_vip_1_name', 'pay_user_vip_2_name', 'pay_user_vip_1_desc', 'pay_user_vip_2_desc', 'vip_1_img_icon', 'vip_2_img_icon']) {
|
||||||
|
assert.ok(!basicJs.includes(key), `vip.js 会员等级组不应再含 ${key}`);
|
||||||
|
assert.ok(!basicPhp.includes(key), `SchemaPayment.php 会员等级组不应再含 ${key}`);
|
||||||
|
}
|
||||||
|
assert.ok(basicJs.includes('pay_user_vip_1_s') && basicJs.includes('pay_user_vip_2_s'), 'vip.js 会员等级组应保留两个等级开关');
|
||||||
|
assert.ok(basicPhp.includes("'key' => 'pay_user_vip_1_s'") && basicPhp.includes("'key' => 'pay_user_vip_2_s'"), 'SchemaPayment.php 会员等级组应保留两个等级开关');
|
||||||
|
// 二级会员先于一级会员开启时的危险提示(参考主题同款)。
|
||||||
|
assert.ok(basicJs.includes('vip_level_order_notice') && basicPhp.includes('vip_level_order_notice'));
|
||||||
|
// 依赖:任一等级启用时显示(zibll pay_user_vip_1_s 依赖的双等级适配)。
|
||||||
|
assert.ok(
|
||||||
|
vipSchemaJs.includes("dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }"),
|
||||||
|
'vip.js 应为 any 双等级依赖'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
schemaPhp.includes("'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]"),
|
||||||
|
'SchemaPayment.php 应为 any 双等级依赖'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('续费/升级介绍字段存在且被档位卡消费', () => {
|
||||||
|
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp]]) {
|
||||||
|
assert.ok(src.includes('vip_renew_desc'), `${name} 应含 vip_renew_desc`);
|
||||||
|
assert.ok(src.includes('vip_upgrade_desc'), `${name} 应含 vip_upgrade_desc`);
|
||||||
|
}
|
||||||
|
// 运行时回退默认值(get_all 不合并 schema 默认值,回退即实际默认值)。
|
||||||
|
assert.ok(paymentRuntime.includes("$settings['vip_renew_desc'] ?? '立即续费会员,畅享VIP权益'"));
|
||||||
|
assert.ok(paymentRuntime.includes("$settings['vip_upgrade_desc'] ?? '升级VIP会员,享更多会员权益'"));
|
||||||
|
// 消费方:/user/vip/ 档位卡按动作取续费/升级简介,不得回退成死配置。
|
||||||
|
assert.ok(vipTabPhp.includes("$vip_settings['renew']['desc']"));
|
||||||
|
assert.ok(vipTabPhp.includes("$vip_settings['upgrade']['desc']"));
|
||||||
|
assert.match(vipTabPhp, /\$card_action = 'renew'/);
|
||||||
|
assert.match(vipTabPhp, /\$card_action = 'upgrade'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('文案默认值三处一致并对齐 zibll', () => {
|
||||||
|
// SPA defaults 块与字段级 default。
|
||||||
|
assert.ok(vipSchemaJs.includes("pay_user_vip_1_name: '黄金会员'"));
|
||||||
|
assert.ok(vipSchemaJs.includes("pay_user_vip_2_name: '钻石会员'"));
|
||||||
|
assert.ok(vipSchemaJs.includes("pay_user_vip_1_desc: '尊享专属特权'"));
|
||||||
|
assert.ok(vipSchemaJs.includes("pay_user_vip_2_desc: '尊享专属特权'"));
|
||||||
|
assert.ok(vipSchemaJs.includes(`pay_user_vip_desc: '${DESC_DEFAULT}'`));
|
||||||
|
assert.ok(vipSchemaJs.includes(`pay_user_vip_more: '${MORE_DEFAULT}'`));
|
||||||
|
// 权益默认值收敛为常量,两份 schema 与运行时共用同一份字面量。
|
||||||
|
assert.ok(vipSchemaJs.includes(`const VIP_EQUITY_DEFAULT = '${EQUITY_DEFAULT}'`));
|
||||||
|
assert.ok(vipSchemaJs.includes('default: VIP_EQUITY_DEFAULT'));
|
||||||
|
// PHP schema 字段 default。
|
||||||
|
assert.ok(schemaPhp.includes("'default' => $level === 1 ? '黄金会员' : '钻石会员'"));
|
||||||
|
assert.ok(schemaPhp.includes(EQUITY_DEFAULT));
|
||||||
|
assert.ok(schemaPhp.includes(`'default' => '${DESC_DEFAULT}'`));
|
||||||
|
assert.ok(schemaPhp.includes(`'default' => '${MORE_DEFAULT}'`));
|
||||||
|
// PaymentRuntime 运行时回退(get_all 不合并 schema 默认值,回退即实际默认值)。
|
||||||
|
assert.ok(paymentRuntime.includes(`$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('${DESC_DEFAULT}', 'aksrt')))`));
|
||||||
|
assert.ok(paymentRuntime.includes(`$more = (string) ($settings['pay_user_vip_more'] ?? '${MORE_DEFAULT}')`));
|
||||||
|
assert.ok(paymentRuntime.includes("$name1 = __('黄金会员', 'aksrt');"));
|
||||||
|
assert.ok(paymentRuntime.includes("$name2 = __('钻石会员', 'aksrt');"));
|
||||||
|
assert.ok(paymentRuntime.includes("$settings['pay_user_vip_1_desc'] ?? __('尊享专属特权', 'aksrt')"));
|
||||||
|
assert.ok(paymentRuntime.includes("$settings['pay_user_vip_2_desc'] ?? __('尊享专属特权', 'aksrt')"));
|
||||||
|
assert.ok(paymentRuntime.includes(EQUITY_DEFAULT));
|
||||||
|
// 旧默认值不再出现在任何一处。
|
||||||
|
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) {
|
||||||
|
assert.ok(!/尊享一级会员特权|尊享二级会员特权|免费下载部分资源|更高免费下载额度/.test(src), `${name} 旧默认值应已移除`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('权益与卡密说明字段保存时保留 HTML', () => {
|
||||||
|
// 权益字段已并入「会员购买选项」的等级 Tab,标签统一为「会员权益 HTML」。
|
||||||
|
assert.ok(vipSchemaJs.includes("key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
|
||||||
|
assert.ok(schemaPhp.includes("['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
|
||||||
|
assert.ok(rechargeJs.includes("key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html'"));
|
||||||
|
assert.ok(schemaPhp.includes("'key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html'"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('会员介绍运行时按单行文本净化', () => {
|
||||||
|
// 只收敛 VIP 设置函数:充值等其它设置的 $desc 与本文无关。
|
||||||
|
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
|
||||||
|
assert.ok(vipSettings.includes("$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc']"));
|
||||||
|
assert.ok(!vipSettings.includes('$desc = sanitize_textarea_field'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('开通会员更多内容只在弹窗内渲染,不裸露在用户中心页面', () => {
|
||||||
|
// 参考主题 zibpay_user_page_tab_content_vip 只渲染 pay_user_vip_desc;
|
||||||
|
// pay_user_vip_more 仅出现在两个弹窗函数(购买/续费弹窗)中。
|
||||||
|
assert.ok(!vipTabPhp.includes("vip_settings['more']"), '用户中心 VIP 页不应渲染 more');
|
||||||
|
assert.ok(!vipTabPhp.includes('muted-box padding-10 mini-radius mt15'), '用户中心 VIP 页不应有 more 提示盒');
|
||||||
|
// 弹窗侧保持:视图模型透传 more + pay-vip.js 右列渲染。
|
||||||
|
assert.match(modalSource, /'more'\s+=>/);
|
||||||
|
assert.match(payVipJs, /catalog\.more/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('more 与卡密说明输出按 post 级 HTML 放行', () => {
|
||||||
|
// 保存端已 wp_kses_post,输出端不能再窄白名单/textarea 净化二次剥标签。
|
||||||
|
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
|
||||||
|
assert.match(vipSettings, /'more'\s+=> wp_kses_post\(\$more\)/);
|
||||||
|
assert.doesNotMatch(vipSettings, /\$allowed_equity \+ \['div'/);
|
||||||
|
const rechargeSettings = slice(paymentRuntime, 'public static function get_payment_recharge_settings', 'public static function get_payment_vip_settings');
|
||||||
|
assert.match(rechargeSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_vip_pass_charge_desc'\]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('弹窗内 more 与卡密说明按 HTML 渲染而非纯文本', () => {
|
||||||
|
// node() 走 textContent 会把标签显示为字面文本;富文本字段必须走 innerHTML。
|
||||||
|
assert.match(payVipJs, /function htmlNode\(tag, className, value\) \{[\s\S]*?el\.innerHTML = text\(value\);/);
|
||||||
|
assert.ok(payVipJs.includes('htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more)'));
|
||||||
|
assert.ok(payVipJs.includes('htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('积分兑换说明按 post 级 HTML 放行并原样渲染', () => {
|
||||||
|
// 参考主题该字段 sanitize=false 且弹窗内原样拼接;AKSRT 对齐为保存/输出
|
||||||
|
// 双端 wp_kses_post + 弹窗 wp_kses_post 渲染。
|
||||||
|
assert.match(vipSchemaJs, /key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html'/);
|
||||||
|
assert.match(schemaPhp, /'key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html'/);
|
||||||
|
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
|
||||||
|
assert.match(vipSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_vip_points_exchange_desc'\]/);
|
||||||
|
// 弹窗渲染端不再 esc_html 剥标签。
|
||||||
|
assert.match(modalSource, /<div class="muted-box muted-2-color padding-10 em09 mt20">' \. wp_kses_post\(\$desc_text\)/);
|
||||||
|
assert.doesNotMatch(modalSource, /esc_html\(\$desc_text\)/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* 会员弹窗骨架屏 / 打开提速 / help_text 清理 / 启动优化契约。
|
||||||
|
*
|
||||||
|
* 硬约束:
|
||||||
|
* 1) 会员弹窗加载占位为完整布局仿形骨架(vipSkeleton()),复用全局
|
||||||
|
* aksrt-modal-runtime__skeleton-line/-block 与 shimmer 动画,
|
||||||
|
* VIP 专属形状样式落在 assets/css/frontend/vip-modal.css;
|
||||||
|
* 2) 弹窗数据缓存 TTL 60s,悬停/聚焦入口按等级意图预热,预热与
|
||||||
|
* openRemote 共用 cacheKey 与 flight;
|
||||||
|
* 3) 弹窗底部提示句与整条 help_text 管线已移除,顶部说明 desc 保留;
|
||||||
|
* 4) 积分/钱包余额合并为一次 LedgerService::points_and_balance();
|
||||||
|
* 5) 访问统计写库移到 shutdown,wp_head 只做访客判定与 setcookie;
|
||||||
|
* 6) header-userbox 样式仅登录用户入队。
|
||||||
|
*/
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..', '..');
|
||||||
|
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
|
||||||
|
|
||||||
|
// 取某个方法体(到下一个同类方法声明为止),避免断言误伤其它功能的同名键。
|
||||||
|
function sliceMethod(source, signature, nextSignature) {
|
||||||
|
const start = source.indexOf(signature);
|
||||||
|
assert.notStrictEqual(start, -1, `missing ${signature}`);
|
||||||
|
const end = source.indexOf(nextSignature, start + signature.length);
|
||||||
|
return source.slice(start, end === -1 ? undefined : end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vipJs = read('assets', 'js', 'pay-vip.js');
|
||||||
|
const vipCss = read('assets', 'css', 'frontend', 'vip-modal.css');
|
||||||
|
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
|
||||||
|
const modal = read('src', 'Content', 'VipServiceModal.php');
|
||||||
|
const catalogSource = read('src', 'Content', 'VipServicePurchase.php');
|
||||||
|
const ledger = read('src', 'Ledger', 'LedgerService.php');
|
||||||
|
const stats = read('src', 'Core', 'StatsTracker.php');
|
||||||
|
const scripts = read('src', 'Core', 'ScriptsPart1b.php');
|
||||||
|
|
||||||
|
test('会员弹窗使用完整布局仿形骨架屏', () => {
|
||||||
|
assert.match(vipJs, /function vipSkeleton\(\) \{/);
|
||||||
|
assert.match(vipJs, /node\("div", "aksrt-vip-modal-skeleton"\)/);
|
||||||
|
assert.match(vipJs, /skeleton\.setAttribute\("aria-busy", "true"\)/);
|
||||||
|
// 结构:用户框(头像+两行)、等级卡行、双列面板(权益+套餐区)。
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__user/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__avatar/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__lines/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__tabs/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__tab/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__pane/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__info/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__products/);
|
||||||
|
assert.match(vipJs, /aksrt-vip-modal-skeleton__button/);
|
||||||
|
// 线条/色块复用全局骨架类。
|
||||||
|
assert.match(vipJs, /aksrt-modal-runtime__skeleton-line is-wide/);
|
||||||
|
assert.match(vipJs, /aksrt-modal-runtime__skeleton-line is-short/);
|
||||||
|
assert.match(vipJs, /aksrt-modal-runtime__skeleton-block/);
|
||||||
|
// openRemote 传入自定义骨架,不再使用纯文本占位。
|
||||||
|
assert.match(vipJs, /skeleton: vipSkeleton\(\),/);
|
||||||
|
assert.doesNotMatch(vipJs, /加载中/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('骨架样式落在 vip-modal.css 并对齐 640px 双列阈值', () => {
|
||||||
|
assert.match(vipCss, /\.aksrt-vip-modal-skeleton \{/);
|
||||||
|
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__avatar \{[\s\S]*?border-radius: 50%/);
|
||||||
|
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__tab \{[\s\S]*?flex: 1/);
|
||||||
|
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__button \{[\s\S]*?height: 38px/);
|
||||||
|
assert.match(vipCss, /animation: aksrt-modal-skeleton/);
|
||||||
|
assert.match(vipCss, /@media \(min-width: 640px\)[\s\S]*?\.aksrt-vip-modal-skeleton__pane \{[\s\S]*?grid-template-columns: 1fr 1\.2fr/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('弹窗打开提速:缓存 TTL 提升与意图预热', () => {
|
||||||
|
assert.match(vipJs, /cacheTtl: 60000,/);
|
||||||
|
// 预热与 openRemote 同一 cacheKey(method + AksrtRequest.url)。
|
||||||
|
assert.match(vipJs, /function vipModalCacheKey\(level\) \{/);
|
||||||
|
assert.match(vipJs, /"GET:" \+ window\.AksrtRequest\.url\("\/pay\/vip\/modal", \{ vip_level: level \}\)/);
|
||||||
|
assert.match(vipJs, /cache\.acquire\(vipModalCacheKey\(level\)/);
|
||||||
|
assert.match(vipJs, /ttl: 60000,/);
|
||||||
|
assert.match(vipJs, /tags: \["vip"\]/);
|
||||||
|
// 悬停/聚焦触发,绑定进 bindOnce。
|
||||||
|
assert.match(vipJs, /\["pointerenter", "focusin"\]/);
|
||||||
|
assert.match(vipJs, /function bindPrefetch\(\) \{/);
|
||||||
|
assert.match(vipJs, /document\.addEventListener\("click", onDocClick, true\);\s*bindPrefetch\(\);/);
|
||||||
|
// 每等级每页只预热一次。
|
||||||
|
assert.match(vipJs, /prefetchedLevels\[level\] = true;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('弹窗底部提示与 help_text 管线已彻底移除', () => {
|
||||||
|
assert.doesNotMatch(vipJs, /help_text/);
|
||||||
|
// 仅收敛 VIP 设置函数:充值/网关设置的 help_text 与本文无关,保持原样。
|
||||||
|
const vipSettings = sliceMethod(
|
||||||
|
paymentRuntime,
|
||||||
|
'public static function get_payment_vip_settings',
|
||||||
|
'public static function get_payment_revenue_settings'
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(vipSettings, /help_text/);
|
||||||
|
assert.doesNotMatch(vipSettings, /开通后立即生效/);
|
||||||
|
assert.doesNotMatch(modal, /help_text/);
|
||||||
|
const viewModel = sliceMethod(modal, 'public function get_modal_view_model', 'public function get_points_exchange_modal_html');
|
||||||
|
assert.doesNotMatch(viewModel, /help_text/);
|
||||||
|
assert.doesNotMatch(catalogSource, /help_text/);
|
||||||
|
// 顶部说明 desc 仍保留(后台 pay_user_vip_desc 继续生效)。
|
||||||
|
assert.match(vipJs, /catalog\.desc/);
|
||||||
|
assert.match(paymentRuntime, /'desc'\s+=> \$desc,/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('积分与钱包余额合并为一次 LedgerService 读取', () => {
|
||||||
|
assert.match(ledger, /public function points_and_balance\(int \$user_id\): array/);
|
||||||
|
// 仅收敛 get_catalog:购买/履约路径的余额读取与本文无关,保持原样。
|
||||||
|
const getCatalog = sliceMethod(catalogSource, 'public function get_catalog', 'private function find_catalog_package');
|
||||||
|
assert.match(getCatalog, /points_and_balance\(\$user_id\)/);
|
||||||
|
assert.match(getCatalog, /'user_points'\s+=> \$balances\['points'\]/);
|
||||||
|
assert.match(getCatalog, /'wallet_balance'\s+=> \$balances\['balance'\]/);
|
||||||
|
assert.doesNotMatch(getCatalog, /LedgerService::get_instance\(\)->balance\(/);
|
||||||
|
assert.doesNotMatch(getCatalog, /balance_decimal/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('访问统计写库移出渲染关键路径', () => {
|
||||||
|
assert.match(stats, /add_action\('wp_head', \[self::get_instance\(\), 'track_visit'\], 1\)/);
|
||||||
|
assert.match(stats, /add_action\('shutdown'/);
|
||||||
|
assert.match(stats, /public function record_stats\(string \$option_key, bool \$is_unique\): void/);
|
||||||
|
// wp_head 阶段只做判定与 setcookie,不再读写 option。
|
||||||
|
const trackVisit = stats.slice(stats.indexOf('public function track_visit'), stats.indexOf('public function record_stats'));
|
||||||
|
assert.doesNotMatch(trackVisit, /update_option|get_option/);
|
||||||
|
assert.match(trackVisit, /setcookie\(\$visitor_cookie/);
|
||||||
|
// 落库与月初清理收敛到 record_stats。
|
||||||
|
const recordStats = stats.slice(stats.indexOf('public function record_stats'));
|
||||||
|
assert.match(recordStats, /update_option\(\$option_key, \$stats, false\)/);
|
||||||
|
assert.match(recordStats, /cleanup_old_stats\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('header-userbox 样式仅登录用户入队', () => {
|
||||||
|
assert.match(scripts, /if \(is_user_logged_in\(\)\) \{\s*\$enqueue_module\('header-userbox'\);/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
/**
|
||||||
|
* VIP 后台设置结构化契约(对照参考主题 zibll 1:1)。
|
||||||
|
*
|
||||||
|
* 硬约束:
|
||||||
|
* 1) 会员商品 / 续费商品 / 升级价格 / 积分兑换明细全部为结构化 group 或
|
||||||
|
* accordion,`|` 分隔的 textarea 配置已彻底移除(不写双路/迁移);
|
||||||
|
* 2) 「会员等级」组只留导航按钮与两个等级开关,等级身份字段(显示名称/
|
||||||
|
* 简介/权益HTML/图标URL)与「会员商品」同处「会员购买选项」的等级 Tab;
|
||||||
|
* 3) 会员商品字段集恰好 6 项:执行价/原价/促销标签/标签颜色/会员有效时间/
|
||||||
|
* 时长单位;标签颜色为参考主题 b+jb 全量 16 色;
|
||||||
|
* 4) 续费价格类型含「自定义商品」,升级价格为参考主题同款三块 accordion;
|
||||||
|
* 5) tag_class 一路透传到前端套餐卡;等级 Tab 标题由显示名称动态驱动。
|
||||||
|
*/
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { pathToFileURL } = require('node:url');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..', '..');
|
||||||
|
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
|
||||||
|
|
||||||
|
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
|
||||||
|
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
|
||||||
|
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
|
||||||
|
const packagesTrait = read('src', 'Content', 'VipServicePackages.php');
|
||||||
|
const purchaseSource = read('src', 'Content', 'VipServicePurchase.php');
|
||||||
|
const modalSource = read('src', 'Content', 'VipServiceModal.php');
|
||||||
|
const rendererVue = read('assets', 'admin', 'src', 'components', 'form', 'AksFormRenderer.vue');
|
||||||
|
const fieldVue = read('assets', 'admin', 'src', 'components', 'form', 'AksField.vue');
|
||||||
|
const payVipJs = read('assets', 'js', 'pay-vip.js');
|
||||||
|
const vipTabPhp = read('inc', 'helpers', 'user-center-renderers', 'vip-tab.php');
|
||||||
|
const homeCms = read('src', 'Settings', 'HomeCms.php');
|
||||||
|
|
||||||
|
const TAG_PALETTE = [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
const PRODUCT_FIELDS = ['price', 'show_price', 'tag', 'tag_class', 'time', 'unit'];
|
||||||
|
const UPGRADE_FIELDS = [
|
||||||
|
'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class',
|
||||||
|
'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class',
|
||||||
|
'permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class',
|
||||||
|
];
|
||||||
|
const LEGACY_KEYS = [
|
||||||
|
'payment_vip_1_products',
|
||||||
|
'payment_vip_2_products',
|
||||||
|
'payment_vip_points_packages',
|
||||||
|
];
|
||||||
|
|
||||||
|
function runHarness() {
|
||||||
|
const out = execFileSync(process.env.PHP_BINARY || 'php', [
|
||||||
|
path.join(__dirname, 'vip-settings-structured-harness.php'),
|
||||||
|
], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
||||||
|
return JSON.parse(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在 SPA schema 里按 key 递归查找字段定义。 */
|
||||||
|
function findGroup(schema, key) {
|
||||||
|
const walk = (nodes) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.key === key) return node
|
||||||
|
if (Array.isArray(node.tabs)) {
|
||||||
|
for (const tab of node.tabs) {
|
||||||
|
const hit = walk(tab.fields || [])
|
||||||
|
if (hit) return hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(node.fields)) {
|
||||||
|
const hit = walk(node.fields)
|
||||||
|
if (hit) return hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return walk(schema.groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只切 VIP 设置函数:充值/积分设置里的 products_raw 与本文无关。
|
||||||
|
function vipSettingsSlice() {
|
||||||
|
const start = paymentRuntime.indexOf('public static function get_payment_vip_settings');
|
||||||
|
assert.notStrictEqual(start, -1, 'missing get_payment_vip_settings');
|
||||||
|
const end = paymentRuntime.indexOf('public static function get_payment_revenue_settings', start);
|
||||||
|
return paymentRuntime.slice(start, end === -1 ? undefined : end);
|
||||||
|
}
|
||||||
|
|
||||||
|
let harness;
|
||||||
|
test('PHP harness 可执行并产出结构化设置', () => {
|
||||||
|
harness = runHarness();
|
||||||
|
assert.ok(harness && harness.schema && harness.runtime, 'harness 输出不完整');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('旧 `|` 分隔配置键已从两份 schema 与运行时彻底移除', () => {
|
||||||
|
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) {
|
||||||
|
for (const key of LEGACY_KEYS) {
|
||||||
|
assert.ok(!src.includes(key), `${name} 不应再含 ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 运行时不再暴露任何 *_raw 原始字符串。
|
||||||
|
const vipRuntime = vipSettingsSlice();
|
||||||
|
for (const key of ['products_raw', 'packages_raw']) {
|
||||||
|
assert.ok(!vipRuntime.includes(`'${key}'`), `PaymentRuntime VIP 设置不应再产出 ${key}`);
|
||||||
|
}
|
||||||
|
assert.ok(!vipRuntime.includes("'product' => sanitize_textarea_field"), 'upgrade.product 管道配置应已移除');
|
||||||
|
for (const key of LEGACY_KEYS) {
|
||||||
|
assert.ok(!vipRuntime.includes(key), `PaymentRuntime VIP 设置不应再读 ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('会员等级组只留导航按钮与等级开关,身份字段搬进等级 Tab', () => {
|
||||||
|
const basic = harness.schema.basic_field_keys;
|
||||||
|
assert.deepStrictEqual(basic, [
|
||||||
|
'nav_pay_vip', 'nav_user_pay_vip', 'pay_user_vip_1_s', 'vip_level_order_notice', 'pay_user_vip_2_s',
|
||||||
|
]);
|
||||||
|
const tabs = harness.schema.opt_tabs;
|
||||||
|
assert.deepStrictEqual(Object.keys(tabs), ['vip1', 'vip2', 'renew', 'upgrade']);
|
||||||
|
for (const level of [1, 2]) {
|
||||||
|
const fields = Object.keys(tabs[`vip${level}`].fields);
|
||||||
|
assert.deepStrictEqual(fields, [
|
||||||
|
`pay_user_vip_${level}_name`,
|
||||||
|
`pay_user_vip_${level}_desc`,
|
||||||
|
`pay_user_vip_${level}_equity`,
|
||||||
|
`vip_${level}_product_s`,
|
||||||
|
`vip_${level}_product`,
|
||||||
|
`vip_${level}_img_icon`,
|
||||||
|
]);
|
||||||
|
assert.strictEqual(tabs[`vip${level}`].fields[`vip_${level}_img_icon`].type, 'upload');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('等级 Tab 标题由显示名称动态驱动', () => {
|
||||||
|
for (const level of [1, 2]) {
|
||||||
|
const tab = harness.schema.opt_tabs[`vip${level}`];
|
||||||
|
assert.strictEqual(tab.titleKey, `pay_user_vip_${level}_name`);
|
||||||
|
assert.strictEqual(tab.titleFallback, level === 1 ? '一级会员' : '二级会员');
|
||||||
|
}
|
||||||
|
// 会员权益的两个等级 Tab 同样动态。
|
||||||
|
assert.strictEqual(harness.schema.benefit_tabs.vip1.titleKey, 'pay_user_vip_1_name');
|
||||||
|
assert.strictEqual(harness.schema.benefit_tabs.vip2.titleKey, 'pay_user_vip_2_name');
|
||||||
|
// 渲染器支持 titleKey / titleFallback。
|
||||||
|
assert.match(rendererVue, /:name="tabName\(tab\)"/);
|
||||||
|
assert.match(rendererVue, /:tab="tabName\(tab\)"/);
|
||||||
|
assert.match(rendererVue, /function tabName\(tab\) \{[\s\S]*?tab\.titleKey[\s\S]*?tab\.titleFallback/);
|
||||||
|
// 手风琴:默认收起,点击标题行展开。
|
||||||
|
assert.match(fieldVue, /const repeaterCollapsible = computed\(\(\) => !!props\.schema\.collapsible\)/);
|
||||||
|
assert.match(fieldVue, /const repeaterDefaultOpen = computed\(\(\) => props\.schema\.defaultCollapsed === false\)/);
|
||||||
|
assert.match(fieldVue, /function toggleRepeaterItem\(index\)/);
|
||||||
|
assert.match(fieldVue, /v-show="!repeaterCollapsible \|\| isRepeaterItemOpen\(index\)"/);
|
||||||
|
assert.match(fieldVue, /aksrt-field__repeater-summary/);
|
||||||
|
// accordion/fieldset 的 sections 分块手风琴。
|
||||||
|
assert.match(fieldVue, /const fieldsetSections = computed\(/);
|
||||||
|
assert.match(fieldVue, /function toggleFieldsetSection\(index\)/);
|
||||||
|
assert.match(fieldVue, /aksrt-field__accordion-section-head/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('结构化分组默认收起,标题行带摘要;升级价格按三块手风琴渲染', () => {
|
||||||
|
const accordion = harness.accordion;
|
||||||
|
// 会员商品 / 续费商品 / 积分兑换明细均默认收起。
|
||||||
|
const expectedGroups = [
|
||||||
|
'vip1/vip_1_product',
|
||||||
|
'vip2/vip_2_product',
|
||||||
|
'renew/vip_1_renew_product',
|
||||||
|
'renew/vip_2_renew_product',
|
||||||
|
'points-exchange/pay_vip_points_exchange_product',
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual(Object.keys(accordion.groups).sort(), [...expectedGroups].sort());
|
||||||
|
for (const key of expectedGroups) {
|
||||||
|
const cfg = accordion.groups[key];
|
||||||
|
assert.strictEqual(cfg.collapsible, true, `${key} 应开启手风琴`);
|
||||||
|
assert.strictEqual(cfg.defaultCollapsed, true, `${key} 应默认收起`);
|
||||||
|
assert.ok(cfg.listPreview && cfg.listPreview.titleKey, `${key} 应配置标题行摘要`);
|
||||||
|
assert.ok(cfg.listPreview.descKey, `${key} 应配置标题行时长摘要`);
|
||||||
|
assert.strictEqual(cfg.listPreview.descUnitKey, 'unit', `${key} 时长摘要应带单位`);
|
||||||
|
}
|
||||||
|
// 会员商品与续费商品摘要前缀为「价格:」,积分兑换为「积分:」。
|
||||||
|
assert.strictEqual(accordion.groups['vip1/vip_1_product'].listPreview.prefix, '价格:');
|
||||||
|
assert.strictEqual(accordion.groups['renew/vip_1_renew_product'].listPreview.prefix, '价格:');
|
||||||
|
assert.strictEqual(accordion.groups['points-exchange/pay_vip_points_exchange_product'].listPreview.prefix, '积分:');
|
||||||
|
assert.strictEqual(accordion.groups['points-exchange/pay_vip_points_exchange_product'].listPreview.titleKey, 'points');
|
||||||
|
|
||||||
|
// 升级价格拆成三块,且每块覆盖的字段并集恰好等于全部字段。
|
||||||
|
const sections = accordion.sections['upgrade/vip_upgrade_product'];
|
||||||
|
assert.strictEqual(sections.length, 3);
|
||||||
|
assert.deepStrictEqual(sections.map((s) => s.title), [
|
||||||
|
'月费会员升级月费会员',
|
||||||
|
'月费会员升级永久会员',
|
||||||
|
'永久会员升级永久会员',
|
||||||
|
]);
|
||||||
|
const covered = sections.flatMap((s) => s.keys).sort();
|
||||||
|
const declared = harness.schema.opt_tabs.upgrade.fields.vip_upgrade_product.fields
|
||||||
|
.map((f) => f.key)
|
||||||
|
.sort();
|
||||||
|
assert.deepStrictEqual(covered, declared, 'sections 必须覆盖升级价格的全部字段');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('会员商品字段集恰好 6 项且标签颜色为参考主题 16 色', () => {
|
||||||
|
const group = harness.schema.opt_tabs.vip1.fields.vip_1_product;
|
||||||
|
assert.strictEqual(group.type, 'group');
|
||||||
|
assert.strictEqual(group.max, 8);
|
||||||
|
assert.strictEqual(group.button_title, '添加会员商品');
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
group.fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||||
|
PRODUCT_FIELDS
|
||||||
|
);
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'price').sanitize_type, 'money_decimal');
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'show_price').sanitize_type, 'money_decimal');
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'time').type, 'spinner');
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'time').max, 3600);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
group.fields.find((f) => f.key === 'unit').options.map((o) => o.value),
|
||||||
|
['day', 'month']
|
||||||
|
);
|
||||||
|
const palette = group.fields.find((f) => f.key === 'tag_class');
|
||||||
|
assert.strictEqual(palette.type, 'palette');
|
||||||
|
assert.strictEqual(palette.default, 'jb-yellow');
|
||||||
|
assert.deepStrictEqual(palette.options.map((o) => o.value), TAG_PALETTE);
|
||||||
|
// 标签颜色仅在填写了促销标签时出现。
|
||||||
|
assert.deepStrictEqual(palette.dependsOn, { key: 'tag', operator: 'not_empty' });
|
||||||
|
// 时长单位在 time 为 0(永久)时隐藏。
|
||||||
|
assert.deepStrictEqual(group.fields.find((f) => f.key === 'unit').dependsOn, { key: 'time', operator: 'truthy' });
|
||||||
|
// 二级会员 Tab 复用同一字段集。
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
harness.schema.opt_tabs.vip2.fields.vip_2_product.fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||||
|
PRODUCT_FIELDS
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('续费价格类型含自定义商品,折扣为折值滑块', () => {
|
||||||
|
const renew = harness.schema.opt_tabs.renew.fields;
|
||||||
|
assert.deepStrictEqual(renew.vip_renew_price_type.options.map((o) => o.value), [
|
||||||
|
'same', 'discount', 'reduce', 'customize',
|
||||||
|
]);
|
||||||
|
const discount = renew.vip_renew_discount;
|
||||||
|
assert.strictEqual(discount.type, 'slider');
|
||||||
|
assert.strictEqual(discount.sanitize_type, 'float');
|
||||||
|
assert.strictEqual(discount.min, 0.1);
|
||||||
|
assert.strictEqual(discount.max, 9.9);
|
||||||
|
assert.strictEqual(discount.step, 0.1);
|
||||||
|
assert.strictEqual(renew.vip_renew_reduce.type, 'spinner');
|
||||||
|
assert.strictEqual(renew.vip_renew_reduce.max, 500);
|
||||||
|
assert.strictEqual(renew.vip_renew_reduce.step, 5);
|
||||||
|
// 自定义续费商品组仅在 customize 时出现。
|
||||||
|
for (const level of [1, 2]) {
|
||||||
|
const group = renew[`vip_${level}_renew_product`];
|
||||||
|
assert.strictEqual(group.type, 'group');
|
||||||
|
assert.strictEqual(group.max, 8);
|
||||||
|
assert.strictEqual(group.button_title, '添加续费商品');
|
||||||
|
assert.deepStrictEqual(group.dependsOn, { key: 'vip_renew_price_type', value: 'customize' });
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
group.fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||||
|
PRODUCT_FIELDS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('升级价格为参考主题同款三块 accordion', () => {
|
||||||
|
const upgrade = harness.schema.opt_tabs.upgrade.fields.vip_upgrade_product;
|
||||||
|
assert.strictEqual(upgrade.type, 'accordion');
|
||||||
|
const keys = upgrade.fields.filter((f) => !f.ui_only).map((f) => f.key);
|
||||||
|
assert.deepStrictEqual(keys, UPGRADE_FIELDS);
|
||||||
|
assert.strictEqual(upgrade.fields.find((f) => f.key === 'month_to_month_s').default, true);
|
||||||
|
assert.strictEqual(upgrade.fields.find((f) => f.key === 'jump_s').default, false);
|
||||||
|
for (const key of ['unit_price', 'jump_price', 'jump_show_price', 'permanent_price', 'permanent_show_price']) {
|
||||||
|
assert.strictEqual(upgrade.fields.find((f) => f.key === key).sanitize_type, 'money_decimal', `${key} 应为 money_decimal`);
|
||||||
|
}
|
||||||
|
for (const key of ['unit_tag_class', 'jump_tag_class', 'permanent_tag_class']) {
|
||||||
|
const field = upgrade.fields.find((f) => f.key === key);
|
||||||
|
assert.strictEqual(field.type, 'palette', `${key} 应为 palette`);
|
||||||
|
assert.deepStrictEqual(field.options.map((o) => o.value), TAG_PALETTE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('积分兑换明细为结构化 group,时长为 time + Permanent 语义', () => {
|
||||||
|
assert.deepStrictEqual(harness.schema.points_fields, [
|
||||||
|
'pay_vip_points_exchange_s', 'pay_vip_points_exchange_product', 'pay_vip_points_exchange_desc',
|
||||||
|
]);
|
||||||
|
const group = harness.schema.points_group;
|
||||||
|
assert.strictEqual(group.type, 'group');
|
||||||
|
assert.strictEqual(group.max, 16);
|
||||||
|
assert.strictEqual(group.button_title, '添加积分兑换商品');
|
||||||
|
assert.deepStrictEqual(group.fields.map((f) => f.key), ['points', 'level', 'time', 'unit']);
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'points').type, 'number');
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'level').type, 'radio');
|
||||||
|
assert.strictEqual(group.fields.find((f) => f.key === 'time').type, 'input');
|
||||||
|
// 永久时隐藏时长单位。
|
||||||
|
assert.deepStrictEqual(group.fields.find((f) => f.key === 'unit').dependsOn, { key: 'time', operator: 'neq', value: 'Permanent' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('积分兑换明细.level 枚举净化后仍能通过校验,非法值依旧报错', () => {
|
||||||
|
const { sanitize } = harness;
|
||||||
|
|
||||||
|
// level 的选项值是整数 1 / 2。默认两行提交数字即可保存,且净化后必须保持
|
||||||
|
// 整数类型:前端 n-radio 用 === 比较保存值,字符串化会让单选框失活。
|
||||||
|
assert.strictEqual(sanitize.default_rows.valid, true);
|
||||||
|
assert.deepStrictEqual(sanitize.default_rows.errors, []);
|
||||||
|
assert.deepStrictEqual(sanitize.default_rows.points_rows.map((r) => r.level), [1, 1]);
|
||||||
|
|
||||||
|
// SPA 提交字符串等级时,同样收敛回选项声明的整数。
|
||||||
|
assert.strictEqual(sanitize.string_level.valid, true);
|
||||||
|
assert.deepStrictEqual(sanitize.string_level.points_rows.map((r) => r.level), [2]);
|
||||||
|
|
||||||
|
// 收敛不得掩盖真正的非法值:level 与 unit 仍各自报出精确错误。
|
||||||
|
assert.strictEqual(sanitize.invalid_level.valid, false);
|
||||||
|
assert.deepStrictEqual(sanitize.invalid_level.errors, ['积分兑换明细[0].level has an invalid value']);
|
||||||
|
assert.strictEqual(sanitize.invalid_unit.valid, false);
|
||||||
|
assert.deepStrictEqual(sanitize.invalid_unit.errors, ['积分兑换明细[0].unit has an invalid value']);
|
||||||
|
|
||||||
|
// 字符串型枚举(palette)不受影响。
|
||||||
|
assert.strictEqual(sanitize.invalid_tag_class.valid, false);
|
||||||
|
assert.deepStrictEqual(sanitize.invalid_tag_class.errors, ['会员商品[0].tag_class has an invalid value']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('运行时归一化:默认值、价格净化、零价与非法行剔除', () => {
|
||||||
|
const defaults = harness.runtime.defaults;
|
||||||
|
// 参考主题默认两行。
|
||||||
|
assert.strictEqual(defaults.levels['1'].products.length, 2);
|
||||||
|
assert.strictEqual(defaults.levels['1'].products[0].price, '99.00');
|
||||||
|
assert.strictEqual(defaults.levels['1'].products[0].time, 3);
|
||||||
|
assert.strictEqual(defaults.levels['1'].products[0].unit, 'month');
|
||||||
|
assert.strictEqual(defaults.levels['1'].products[0].days, 90);
|
||||||
|
assert.strictEqual(defaults.levels['1'].renew_products[0].price, '69.00');
|
||||||
|
assert.strictEqual(defaults.levels['2'].renew_products[0].price, '269.00');
|
||||||
|
// 积分兑换默认两行(含 Permanent)。
|
||||||
|
assert.strictEqual(defaults.points_exchange.packages.length, 2);
|
||||||
|
assert.strictEqual(defaults.points_exchange.packages[1].is_permanent, true);
|
||||||
|
assert.strictEqual(defaults.points_exchange.packages[1].days, 0);
|
||||||
|
// 续费/升级默认值。
|
||||||
|
assert.strictEqual(defaults.renew.price_type, 'discount');
|
||||||
|
assert.strictEqual(defaults.renew.discount, 8);
|
||||||
|
assert.strictEqual(defaults.upgrade.month_to_month_s, true);
|
||||||
|
assert.strictEqual(defaults.upgrade.jump_s, false);
|
||||||
|
assert.strictEqual(defaults.upgrade.unit_price, '10.00');
|
||||||
|
assert.strictEqual(defaults.upgrade.unit_tag_class, 'jb-yellow');
|
||||||
|
assert.strictEqual(defaults.upgrade.jump_tag_class, 'jb-red');
|
||||||
|
|
||||||
|
const custom = harness.runtime.custom;
|
||||||
|
// 零价行被剔除;价格规范化为两位小数。
|
||||||
|
assert.strictEqual(custom.levels['1'].products.length, 2);
|
||||||
|
assert.strictEqual(custom.levels['1'].products[0].price, '88.80');
|
||||||
|
assert.strictEqual(custom.levels['1'].products[0].tag_class, 'jb-green');
|
||||||
|
// 非法标签颜色回落为空串。
|
||||||
|
assert.strictEqual(custom.levels['2'].products[0].tag_class, '');
|
||||||
|
// 自定义续费价格类型与折扣。
|
||||||
|
assert.strictEqual(custom.renew.price_type, 'customize');
|
||||||
|
assert.strictEqual(custom.renew.discount, 7.5);
|
||||||
|
assert.strictEqual(custom.renew.desc, '续费有礼');
|
||||||
|
assert.strictEqual(custom.levels['1'].renew_products[0].tag_class, 'b-red');
|
||||||
|
// 升级三块参数透传。
|
||||||
|
assert.strictEqual(custom.upgrade.unit_price, '12.50');
|
||||||
|
assert.strictEqual(custom.upgrade.unit_tag_class, 'jb-blue');
|
||||||
|
assert.strictEqual(custom.upgrade.jump_s, true);
|
||||||
|
assert.strictEqual(custom.upgrade.permanent_tag_class, 'jb-vip2');
|
||||||
|
// 积分兑换:非法行(积分为 0 / 等级越界 / 时长为空)被剔除。
|
||||||
|
assert.strictEqual(custom.points_exchange.packages.length, 3);
|
||||||
|
assert.strictEqual(custom.points_exchange.packages[0].days, 30);
|
||||||
|
assert.strictEqual(custom.points_exchange.packages[1].is_permanent, true);
|
||||||
|
assert.strictEqual(custom.points_exchange.packages[2].days, 90);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('旧 `|` 管道存量值按新默认值回退,不会渲染成假条目', () => {
|
||||||
|
const legacy = harness.runtime.legacy;
|
||||||
|
assert.strictEqual(legacy.levels['1'].products.length, 2);
|
||||||
|
assert.strictEqual(legacy.levels['1'].products[0].price, '99.00');
|
||||||
|
assert.strictEqual(legacy.points_exchange.packages.length, 2);
|
||||||
|
assert.strictEqual(legacy.upgrade.unit_price, '10.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('续费计价:折值/立减/自定义商品三种语义', () => {
|
||||||
|
const p = harness.pricing;
|
||||||
|
// 相同价格:不改价、不展示原价。
|
||||||
|
assert.strictEqual(p.same.price, '100.00');
|
||||||
|
assert.strictEqual(p.same.show_price, '0.00');
|
||||||
|
// 8 折:执行价 80,原价划线为 100。
|
||||||
|
assert.strictEqual(p.discount_8.price, '80.00');
|
||||||
|
assert.strictEqual(p.discount_8.show_price, '100.00');
|
||||||
|
assert.strictEqual(p.discount_8.tag, '8折优惠');
|
||||||
|
// 支持小数折。
|
||||||
|
assert.strictEqual(p.discount_7_5.price, '75.00');
|
||||||
|
assert.strictEqual(p.discount_7_5.tag, '7.5折优惠');
|
||||||
|
// 超出滑块上限时按 9.9 折钳制(参考主题 slider max)。
|
||||||
|
assert.strictEqual(p.discount_10_clamped.price, '99.00');
|
||||||
|
assert.strictEqual(p.discount_10_clamped.tag, '9.9折优惠');
|
||||||
|
// 立减 20:执行价 80。
|
||||||
|
assert.strictEqual(p.reduce_20.price, '80.00');
|
||||||
|
assert.strictEqual(p.reduce_20.show_price, '100.00');
|
||||||
|
assert.strictEqual(p.reduce_20.tag, '续费立减¥20.00');
|
||||||
|
// 自定义商品:沿用商品自身的原价、促销标签与标签颜色。
|
||||||
|
assert.strictEqual(p.customize.price, '100.00');
|
||||||
|
assert.strictEqual(p.customize.show_price, '200.00');
|
||||||
|
assert.strictEqual(p.customize.tag, '限时');
|
||||||
|
assert.strictEqual(p.customize.tag_class, 'jb-red');
|
||||||
|
// 开通动作不套用续费规则。
|
||||||
|
assert.strictEqual(p.pay.price, '100.00');
|
||||||
|
assert.strictEqual(p.pay.show_price, '0.00');
|
||||||
|
assert.strictEqual(p.pay.tag, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tag_class 透传到前端套餐卡与积分兑换弹窗', () => {
|
||||||
|
assert.match(payVipJs, /item\.tag_class \|\| "jb-yellow"/);
|
||||||
|
// 弹窗时长行按 time/unit 渲染,Permanent 走永久。
|
||||||
|
assert.match(modalSource, /\$is_permanent = !empty\(\$package\['is_permanent'\]\)/);
|
||||||
|
assert.match(modalSource, /\$pkg_unit === 'month' \? __\('个月', 'aksrt'\) : __\('天', 'aksrt'\)/);
|
||||||
|
// 用户中心档位卡消费续费/升级简介。
|
||||||
|
assert.match(vipTabPhp, /\$renew_desc = \(string\) \(\$vip_settings\['renew'\]\['desc'\]/);
|
||||||
|
assert.match(vipTabPhp, /\$upgrade_desc = \(string\) \(\$vip_settings\['upgrade'\]\['desc'\]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('管道解析死代码已清理', () => {
|
||||||
|
for (const [name, src] of [['VipServicePackages.php', packagesTrait], ['VipServicePurchase.php', purchaseSource]]) {
|
||||||
|
assert.ok(!/function parse_level_products/.test(src), `${name} 不应再定义 parse_level_products`);
|
||||||
|
assert.ok(!/function parse_packages\(/.test(src), `${name} 不应再定义 parse_packages`);
|
||||||
|
assert.ok(!/function parse_upgrade_product_line/.test(src), `${name} 不应再定义 parse_upgrade_product_line`);
|
||||||
|
assert.ok(!/preg_split\('\/\\r\\n\|\\r\|\\n\/'/.test(src), `${name} 不应再按行拆分管道配置`);
|
||||||
|
}
|
||||||
|
// HomeCms 的零调用 PaymentRuntime 代理方法已删除。
|
||||||
|
for (const method of ['get_payment_vip_settings', 'get_payment_recharge_settings', 'get_withdrawal_settings']) {
|
||||||
|
assert.ok(!homeCms.includes(`function ${method}`), `HomeCms 不应再含死代理 ${method}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SPA 镜像 schema 与 PHP schema 同构', async () => {
|
||||||
|
const mod = await import(pathToFileURL(path.join(root, 'assets/admin/src/schema/sections/payment/vip.js')).href);
|
||||||
|
const schema = mod.vipSchema;
|
||||||
|
assert.deepStrictEqual(schema.groups.map((g) => g.key), [
|
||||||
|
'notice', 'basic', 'benefit', 'opt', 'points-exchange', 'modal-copy',
|
||||||
|
]);
|
||||||
|
const opt = schema.groups.find((g) => g.key === 'opt').fields[0];
|
||||||
|
assert.strictEqual(opt.type, 'tabbed');
|
||||||
|
assert.deepStrictEqual(opt.tabs.map((t) => t.key), ['vip1', 'vip2', 'renew', 'upgrade']);
|
||||||
|
for (const level of [1, 2]) {
|
||||||
|
const tab = opt.tabs[level - 1];
|
||||||
|
assert.strictEqual(tab.titleKey, `pay_user_vip_${level}_name`);
|
||||||
|
assert.deepStrictEqual(tab.fields.map((f) => f.key), [
|
||||||
|
`pay_user_vip_${level}_name`,
|
||||||
|
`pay_user_vip_${level}_desc`,
|
||||||
|
`pay_user_vip_${level}_equity`,
|
||||||
|
`vip_${level}_product_s`,
|
||||||
|
`vip_${level}_product`,
|
||||||
|
`vip_${level}_img_icon`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
opt.tabs[2].fields.find((f) => f.key === 'vip_renew_price_type').options.map((o) => o.value),
|
||||||
|
['same', 'discount', 'reduce', 'customize']
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
opt.tabs[3].fields.find((f) => f.key === 'vip_upgrade_product').fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||||
|
UPGRADE_FIELDS
|
||||||
|
);
|
||||||
|
const points = schema.groups.find((g) => g.key === 'points-exchange').fields[1];
|
||||||
|
assert.strictEqual(points.type, 'group');
|
||||||
|
assert.deepStrictEqual(points.fields.map((f) => f.key), ['points', 'level', 'time', 'unit']);
|
||||||
|
// 手风琴配置与 PHP 侧一致。
|
||||||
|
for (const key of ['vip_1_product', 'vip_2_product', 'vip_1_renew_product', 'vip_2_renew_product']) {
|
||||||
|
const group = findGroup(schema, key);
|
||||||
|
assert.strictEqual(group.collapsible, true, `${key} 应开启手风琴`);
|
||||||
|
assert.strictEqual(group.defaultCollapsed, true, `${key} 应默认收起`);
|
||||||
|
}
|
||||||
|
assert.strictEqual(findGroup(schema, 'pay_vip_points_exchange_product').collapsible, true);
|
||||||
|
const upgrade = findGroup(schema, 'vip_upgrade_product');
|
||||||
|
assert.strictEqual(upgrade.sections.length, 3);
|
||||||
|
// SPA 默认值与 PHP 侧一致。
|
||||||
|
assert.strictEqual(schema.defaults.vip_1_product[0].price, 99);
|
||||||
|
assert.strictEqual(schema.defaults.vip_1_renew_product[0].price, 69);
|
||||||
|
assert.strictEqual(schema.defaults.vip_2_renew_product[0].price, 269);
|
||||||
|
assert.strictEqual(schema.defaults.vip_renew_discount, 8);
|
||||||
|
assert.strictEqual(schema.defaults.vip_upgrade_product.jump_s, false);
|
||||||
|
// 旧键不出现在 SPA schema。
|
||||||
|
for (const key of LEGACY_KEYS) {
|
||||||
|
assert.ok(!vipSchemaJs.includes(key), `vip.js 不应再含 ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('升级价格 sections 能展开成三块手风琴,且块内字段遵循 dependsOn', async () => {
|
||||||
|
const { buildFieldsetSections } = await import(
|
||||||
|
pathToFileURL(path.join(root, 'assets/admin/src/components/form/fieldset-sections.js')).href
|
||||||
|
);
|
||||||
|
const mod = await import(pathToFileURL(path.join(root, 'assets/admin/src/schema/sections/payment/vip.js')).href);
|
||||||
|
const upgrade = findGroup(mod.vipSchema, 'vip_upgrade_product');
|
||||||
|
|
||||||
|
const sections = buildFieldsetSections(upgrade);
|
||||||
|
assert.strictEqual(sections.length, 3, '升级价格应展开成三块');
|
||||||
|
assert.deepStrictEqual(sections.map((s) => s.title), [
|
||||||
|
'月费会员升级月费会员',
|
||||||
|
'月费会员升级永久会员',
|
||||||
|
'永久会员升级永久会员',
|
||||||
|
]);
|
||||||
|
// 每块的字段并集恰好等于声明的全部字段,且顺序与 schema.fields 一致。
|
||||||
|
const expanded = sections.flatMap((s) => s.fields.map((f) => f.key));
|
||||||
|
assert.deepStrictEqual(expanded, upgrade.fields.map((f) => f.key));
|
||||||
|
// 第一块带「已设置为永久会员」之外的提示与开关,字段定义必须是真实对象而非占位。
|
||||||
|
assert.ok(sections[0].fields.every((f) => f && typeof f === 'object' && f.key));
|
||||||
|
|
||||||
|
// 没有 sections 声明的 fieldset 保持平铺,不产生假块。
|
||||||
|
assert.deepStrictEqual(buildFieldsetSections({ type: 'fieldset', fields: [{ key: 'a' }] }), []);
|
||||||
|
assert.deepStrictEqual(buildFieldsetSections(undefined), []);
|
||||||
|
|
||||||
|
// 渲染层把 sections 交给纯函数,并对块内字段套用 dependsOn。
|
||||||
|
assert.match(fieldVue, /const fieldsetSections = computed\(\(\) => \{[\s\S]*?buildFieldsetSections\(props\.schema\)/);
|
||||||
|
assert.match(fieldVue, /function sectionFields\(section\) \{[\s\S]*?matchDependency\(field\.dependsOn, props\.modelValue\)/);
|
||||||
|
// 结构化分组之间留出间隔,两个分组不会上下贴在一起。
|
||||||
|
assert.match(fieldVue, /\.aksrt-field--group,\s*\.aksrt-field--repeater,\s*\.aksrt-field--accordion,\s*\.aksrt-field--fieldset \{[\s\S]*?margin-bottom: 26px;/);
|
||||||
|
assert.match(fieldVue, /\.aksrt-field__accordion-section \+ \.aksrt-field__accordion-section \{[\s\S]*?margin-top: 14px;/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Executable VIP structured-settings contract harness.
|
||||||
|
*
|
||||||
|
* Builds the payment schema with WordPress stubs, normalizes a stored
|
||||||
|
* `aksrt_settings` snapshot through PaymentRuntime, and prints the result as
|
||||||
|
* JSON so the Node contract test can assert on the real shapes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
define('ABSPATH', __DIR__ . '/');
|
||||||
|
define('DAY_IN_SECONDS', 86400);
|
||||||
|
|
||||||
|
function __(string $text, string $domain = ''): string { return $text; }
|
||||||
|
function _x(string $text, string $context, string $domain = ''): string { return $text; }
|
||||||
|
function esc_html($value): string { return (string) $value; }
|
||||||
|
function esc_attr($value): string { return (string) $value; }
|
||||||
|
function esc_url($value): string { return (string) $value; }
|
||||||
|
function esc_url_raw($value): string { return trim((string) $value); }
|
||||||
|
function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
|
||||||
|
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
|
||||||
|
function sanitize_textarea_field($value): string { return trim((string) $value); }
|
||||||
|
function wp_kses_post($value): string { return (string) $value; }
|
||||||
|
function wp_kses($value, $allowed = null): string { return (string) $value; }
|
||||||
|
function trailingslashit($value): string { return rtrim((string) $value, '/\\') . '/'; }
|
||||||
|
function get_template_directory_uri(): string { return 'https://example.test/wp-content/themes/aksrt'; }
|
||||||
|
function absint($value): int { return max(0, (int) $value); }
|
||||||
|
function wp_json_encode($value, int $flags = 0): string { return json_encode($value, $flags | JSON_UNESCAPED_SLASHES); }
|
||||||
|
function is_multisite(): bool { return false; }
|
||||||
|
function get_option(string $key, $default = false) { return $default; }
|
||||||
|
function did_action(string $hook): int { return 0; }
|
||||||
|
function apply_filters(string $hook, $value, ...$args) { return $value; }
|
||||||
|
function add_action(...$args): void {}
|
||||||
|
function add_filter(...$args): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace AKSRT\Settings {
|
||||||
|
/** Test double: PaymentRuntime reads every raw settings key through this. */
|
||||||
|
final class Settings {
|
||||||
|
/** @var array<string,mixed> */
|
||||||
|
public static array $snapshot = [];
|
||||||
|
|
||||||
|
public static function get_all(): array {
|
||||||
|
return self::$snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_paid_content_settings(): array {
|
||||||
|
return ['display' => ['currency_symbol' => '¥']];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function reset(): void {
|
||||||
|
self::$snapshot = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
require dirname(__DIR__, 2) . '/src/Ledger/AssetType.php';
|
||||||
|
require dirname(__DIR__, 2) . '/src/Ledger/AssetAmount.php';
|
||||||
|
require dirname(__DIR__, 2) . '/src/Settings/PaymentRuntime.php';
|
||||||
|
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaPayment.php';
|
||||||
|
require dirname(__DIR__, 2) . '/src/Admin/SchemaProvider.php';
|
||||||
|
require dirname(__DIR__, 2) . '/src/Content/VipServicePackages.php';
|
||||||
|
|
||||||
|
/** Exposes the trait's private renew-pricing math for contract assertions. */
|
||||||
|
final class VipPricingProbe {
|
||||||
|
use \AKSRT\Content\VipServicePackages;
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $pkg */
|
||||||
|
public function price(array $pkg, string $action, array $renew, int $days): ?array {
|
||||||
|
return $this->apply_action_price($pkg, $action, $renew, [], 1, 1, 0, false, $days);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use AKSRT\Admin\Schema\SchemaPayment;
|
||||||
|
use AKSRT\Settings\PaymentRuntime;
|
||||||
|
use AKSRT\Settings\Settings;
|
||||||
|
|
||||||
|
$vip = SchemaPayment::build()['sections']['vip'];
|
||||||
|
$groups = [];
|
||||||
|
foreach ($vip['groups'] as $group) {
|
||||||
|
$groups[$group['key']] = $group;
|
||||||
|
}
|
||||||
|
|
||||||
|
$opt_tabs = [];
|
||||||
|
foreach ($groups['opt']['fields'][0]['tabs'] as $tab) {
|
||||||
|
$fields = [];
|
||||||
|
foreach ($tab['fields'] as $field) {
|
||||||
|
$fields[$field['key']] = $field;
|
||||||
|
}
|
||||||
|
$opt_tabs[$tab['key']] = [
|
||||||
|
'title' => $tab['title'],
|
||||||
|
'titleKey' => $tab['titleKey'] ?? null,
|
||||||
|
'titleFallback' => $tab['titleFallback'] ?? null,
|
||||||
|
'fields' => $fields,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$benefit_tabs = [];
|
||||||
|
foreach ($groups['benefit']['fields'][0]['tabs'] as $tab) {
|
||||||
|
$benefit_tabs[$tab['key']] = [
|
||||||
|
'title' => $tab['title'],
|
||||||
|
'titleKey' => $tab['titleKey'] ?? null,
|
||||||
|
'titleFallback' => $tab['titleFallback'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- runtime: defaults (nothing stored) -------------------------------
|
||||||
|
Settings::reset();
|
||||||
|
$defaults = PaymentRuntime::get_payment_vip_settings();
|
||||||
|
|
||||||
|
// ---- runtime: fully customised stored snapshot -------------------------
|
||||||
|
Settings::$snapshot = [
|
||||||
|
'pay_user_vip_1_s' => true,
|
||||||
|
'pay_user_vip_2_s' => true,
|
||||||
|
'pay_user_vip_1_name' => '青铜会员',
|
||||||
|
'pay_user_vip_2_name' => '王者会员',
|
||||||
|
'vip_renew' => true,
|
||||||
|
'vip_renew_desc' => '续费有礼',
|
||||||
|
'vip_renew_price_type' => 'customize',
|
||||||
|
'vip_renew_discount' => 7.5,
|
||||||
|
'vip_renew_reduce' => '15.00',
|
||||||
|
'vip_upgrade' => true,
|
||||||
|
'vip_upgrade_desc' => '升级王者',
|
||||||
|
'vip_upgrade_product' => [
|
||||||
|
'month_to_month_s' => true,
|
||||||
|
'unit_price' => '12.50',
|
||||||
|
'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 按天升级',
|
||||||
|
'unit_tag_class' => 'jb-blue',
|
||||||
|
'jump_s' => true,
|
||||||
|
'jump_price' => '259.00',
|
||||||
|
'jump_show_price' => '399.00',
|
||||||
|
'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 直升永久',
|
||||||
|
'jump_tag_class' => 'jb-red',
|
||||||
|
'permanent_price' => '159.00',
|
||||||
|
'permanent_show_price' => '259.00',
|
||||||
|
'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 王者特惠',
|
||||||
|
'permanent_tag_class' => 'jb-vip2',
|
||||||
|
],
|
||||||
|
'vip_1_product' => [
|
||||||
|
['price' => '88.80', 'show_price' => '188.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 新客', 'tag_class' => 'jb-green', 'time' => 1, 'unit' => 'month'],
|
||||||
|
['price' => '0', 'show_price' => '', 'tag' => '', 'time' => 0, 'unit' => 'day'],
|
||||||
|
['price' => '10', 'show_price' => '20', 'tag' => '', 'time' => 2, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
'vip_2_product' => [
|
||||||
|
['price' => '300', 'show_price' => '600', 'tag' => 'x', 'tag_class' => 'not-a-real-class', 'time' => 12, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
'vip_1_renew_product' => [
|
||||||
|
['price' => '60', 'show_price' => '99', 'tag' => '续费专享', 'tag_class' => 'b-red', 'time' => 1, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
'vip_2_renew_product' => [],
|
||||||
|
'pay_vip_points_exchange_s' => true,
|
||||||
|
'pay_vip_points_exchange_product' => [
|
||||||
|
['points' => 800, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||||
|
['points' => 9000, 'level' => 2, 'time' => 'Permanent'],
|
||||||
|
['points' => 0, 'level' => 1, 'time' => 10, 'unit' => 'day'],
|
||||||
|
['points' => 100, 'level' => 9, 'time' => 10, 'unit' => 'day'],
|
||||||
|
['points' => 100, 'level' => 1, 'time' => '', 'unit' => 'day'],
|
||||||
|
['points' => 120, 'level' => 2, 'time' => 3, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$custom = PaymentRuntime::get_payment_vip_settings();
|
||||||
|
|
||||||
|
// ---- legacy scalar payloads must fall back to defaults -----------------
|
||||||
|
Settings::$snapshot = [
|
||||||
|
'pay_user_vip_1_s' => true,
|
||||||
|
'vip_1_product' => "30|59|1|month|限时|月卡",
|
||||||
|
'pay_vip_points_exchange_product' => "1|500|30|积分月卡",
|
||||||
|
'vip_upgrade_product' => "unit|1|按剩余天数升级",
|
||||||
|
];
|
||||||
|
$legacy = PaymentRuntime::get_payment_vip_settings();
|
||||||
|
|
||||||
|
// ---- sanitize -> validate round-trip -----------------------------------
|
||||||
|
// 复现并锁定:积分兑换明细.level 的选项值为整数,而 SPA 提交的可能是数字
|
||||||
|
// 或字符串;净化必须把值收敛回选项声明的类型,否则严格 in_array 校验会以
|
||||||
|
// "has an invalid value" 拒绝一次完全合法的保存。
|
||||||
|
$sanitize = (static function (): array {
|
||||||
|
$provider = \AKSRT\Admin\SchemaProvider::get_instance();
|
||||||
|
$cases = [
|
||||||
|
'default_rows' => [
|
||||||
|
'pay_vip_points_exchange_s' => true,
|
||||||
|
'pay_vip_points_exchange_product' => [
|
||||||
|
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||||
|
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'string_level' => [
|
||||||
|
'pay_vip_points_exchange_s' => true,
|
||||||
|
'pay_vip_points_exchange_product' => [
|
||||||
|
['points' => 800, 'level' => '2', 'time' => 3, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'invalid_level' => [
|
||||||
|
'pay_vip_points_exchange_s' => true,
|
||||||
|
'pay_vip_points_exchange_product' => [
|
||||||
|
['points' => 500, 'level' => 9, 'time' => 30, 'unit' => 'day'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'invalid_unit' => [
|
||||||
|
'pay_vip_points_exchange_s' => true,
|
||||||
|
'pay_vip_points_exchange_product' => [
|
||||||
|
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'year'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'invalid_tag_class' => [
|
||||||
|
'vip_1_product' => [
|
||||||
|
['price' => '88.80', 'show_price' => '99', 'tag' => 'x', 'tag_class' => 'nope', 'time' => 1, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$out = [];
|
||||||
|
foreach ($cases as $name => $payload) {
|
||||||
|
$clean = $provider->sanitize_payload('payment', $payload, 'vip');
|
||||||
|
$result = $provider->validate('payment', $clean, 'vip');
|
||||||
|
$out[$name] = [
|
||||||
|
'points_rows' => $clean['pay_vip_points_exchange_product'] ?? null,
|
||||||
|
'valid' => $result['valid'],
|
||||||
|
'errors' => $result['errors'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
})();
|
||||||
|
|
||||||
|
echo wp_json_encode([
|
||||||
|
'schema' => [
|
||||||
|
'version' => $vip['version'],
|
||||||
|
'group_keys' => array_column($vip['groups'], 'key'),
|
||||||
|
'basic_field_keys' => array_column($groups['basic']['fields'], 'key'),
|
||||||
|
'benefit_tabs' => $benefit_tabs,
|
||||||
|
'opt_tabs' => $opt_tabs,
|
||||||
|
'points_fields' => array_column($groups['points-exchange']['fields'], 'key'),
|
||||||
|
'points_group' => $groups['points-exchange']['fields'][1],
|
||||||
|
'tag_palette' => $opt_tabs['vip1']['fields']['vip_1_product']['fields'][3]['options'],
|
||||||
|
],
|
||||||
|
'runtime' => [
|
||||||
|
'defaults' => $defaults,
|
||||||
|
'custom' => $custom,
|
||||||
|
'legacy' => $legacy,
|
||||||
|
],
|
||||||
|
'sanitize' => $sanitize,
|
||||||
|
'accordion' => (static function (array $groups): array {
|
||||||
|
$out = ['groups' => [], 'sections' => []];
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
foreach ($group['fields'] as $top) {
|
||||||
|
if (($top['type'] ?? '') !== 'tabbed') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($top['tabs'] as $tab) {
|
||||||
|
foreach ($tab['fields'] as $field) {
|
||||||
|
if (($field['type'] ?? '') === 'group') {
|
||||||
|
$out['groups'][$tab['key'] . '/' . $field['key']] = [
|
||||||
|
'collapsible' => $field['collapsible'] ?? null,
|
||||||
|
'defaultCollapsed' => $field['defaultCollapsed'] ?? null,
|
||||||
|
'listPreview' => $field['listPreview'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (($field['type'] ?? '') === 'accordion') {
|
||||||
|
$out['sections'][$tab['key'] . '/' . $field['key']] = array_map(
|
||||||
|
static fn(array $section): array => [
|
||||||
|
'title' => $section['title'],
|
||||||
|
'keys' => $section['keys'],
|
||||||
|
],
|
||||||
|
$field['sections'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
if ($group['key'] !== 'points-exchange') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($group['fields'] as $field) {
|
||||||
|
if (($field['type'] ?? '') !== 'group') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out['groups']['points-exchange/' . $field['key']] = [
|
||||||
|
'collapsible' => $field['collapsible'] ?? null,
|
||||||
|
'defaultCollapsed' => $field['defaultCollapsed'] ?? null,
|
||||||
|
'listPreview' => $field['listPreview'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
})($groups),
|
||||||
|
'pricing' => (static function (): array {
|
||||||
|
$probe = new VipPricingProbe();
|
||||||
|
$base = [
|
||||||
|
'price' => '100.00', 'show_price' => '200.00', 'tag' => '限时',
|
||||||
|
'tag_class' => 'jb-red', 'time' => 1, 'unit' => 'month',
|
||||||
|
];
|
||||||
|
return [
|
||||||
|
'same' => $probe->price($base, 'renew', ['price_type' => 'same'], 30),
|
||||||
|
'discount_8' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 8], 30),
|
||||||
|
'discount_7_5' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 7.5], 30),
|
||||||
|
'discount_10_clamped' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 10], 30),
|
||||||
|
'reduce_20' => $probe->price($base, 'renew', ['price_type' => 'reduce', 'reduce' => '20.00'], 30),
|
||||||
|
'customize' => $probe->price($base, 'renew', ['price_type' => 'customize'], 30),
|
||||||
|
'pay' => $probe->price($base, 'pay', ['price_type' => 'discount'], 30),
|
||||||
|
];
|
||||||
|
})(),
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), PHP_EOL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 用户中心 VIP 页状态卡契约(inc/helpers/user-center-renderers/vip-tab.php)。
|
||||||
|
*
|
||||||
|
* 硬约束:
|
||||||
|
* 1) 未开通用户不渲染到期时间行——状态标签已显示「未开通」,参考主题
|
||||||
|
* zibpay_user_page_tab_content_vip 对从未开通用户同样不输出任何到期
|
||||||
|
* 信息(仅生效中/永久/已过期才有);
|
||||||
|
* 2) 生效中、永久、已过期三个分支的到期文案保持产出;
|
||||||
|
* 3) 到期行整体按文案非空条件渲染,不留空壳 div。
|
||||||
|
*/
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..', '..');
|
||||||
|
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
|
||||||
|
|
||||||
|
const vipTab = read('inc', 'helpers', 'user-center-renderers', 'vip-tab.php');
|
||||||
|
|
||||||
|
test('未开通用户不渲染到期时间行', () => {
|
||||||
|
// 未开通初始化不再产出「到期时间:未开通」文案。
|
||||||
|
assert.ok(!vipTab.includes('到期时间:未开通'), '未开通不应再产出到期文案');
|
||||||
|
assert.match(vipTab, /\$vip_expiry_text\s*=\s*'';/);
|
||||||
|
// 到期行按文案非空条件渲染。
|
||||||
|
assert.match(vipTab, /\$vip_expiry_text !== '' \? '<div class="uc-vip-status__expiry">/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('生效中/永久/已过期分支仍产出到期文案', () => {
|
||||||
|
assert.match(vipTab, /到期时间:永久会员/);
|
||||||
|
assert.match(vipTab, /到期时间:%s/);
|
||||||
|
assert.match(vipTab, /到期时间:已过期/);
|
||||||
|
// 三个分支都会给 $vip_expiry_text 赋值。
|
||||||
|
const assignments = vipTab.match(/\$vip_expiry_text\s*=/g) || [];
|
||||||
|
assert.ok(assignments.length >= 4, '初始化 + 永久 + 生效中 + 已过期均应赋值');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未开通状态名称前不渲染图标位', () => {
|
||||||
|
// 图标位仅在有等级(生效中/永久/已过期)时输出;未开通(level 0)不输出,
|
||||||
|
// 避免「未开通会员」前面挂通用证书图标。
|
||||||
|
assert.match(vipTab, /\$vip_level_number > 0 \? '<span class="uc-vip-status__icon"/);
|
||||||
|
assert.ok(!vipTab.includes(". '<span class=\"uc-vip-status__icon\" aria-hidden=\"true\">' . \$status_icon"), '图标位不应再无条件渲染');
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-10-03T20:06:15+00:00",
|
"generated_at": "2026-10-13T12:53:18+00:00",
|
||||||
"errors": [],
|
"errors": [],
|
||||||
"warnings": [
|
"warnings": [
|
||||||
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
"enum incomplete parse for 'search_type_in' (php subset of js): php=[post] js=[post,user]",
|
"enum incomplete parse for 'search_type_in' (php subset of js): php=[post] js=[post,user]",
|
||||||
"enum incomplete parse for 'sign_card_position' (php subset of js): php=[left] js=[left,center,right]",
|
"enum incomplete parse for 'sign_card_position' (php subset of js): php=[left] js=[left,center,right]",
|
||||||
"enum incomplete parse for 'sign_theme_mode' (php subset of js): php=[no] js=[no,white-theme,dark-theme]",
|
"enum incomplete parse for 'sign_theme_mode' (php subset of js): php=[no] js=[no,white-theme,dark-theme]",
|
||||||
|
"enum incomplete parse for 'sms_ali_api_type' (php subset of js): php=[sms] js=[sms,verify]",
|
||||||
|
"enum incomplete parse for 'sms_sdk' (php subset of js): php=[null] js=[null,ali,tencent,smsbao]",
|
||||||
"enum incomplete parse for 'style' (php subset of js): php=[null] js=[null,style2]",
|
"enum incomplete parse for 'style' (php subset of js): php=[null] js=[null,style2]",
|
||||||
"enum incomplete parse for 'system_notice_btn1_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
"enum incomplete parse for 'system_notice_btn1_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
||||||
"enum incomplete parse for 'system_notice_btn2_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
"enum incomplete parse for 'system_notice_btn2_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
||||||
@@ -57,12 +59,12 @@
|
|||||||
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
|
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
|
||||||
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
|
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
|
||||||
"enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]",
|
"enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]",
|
||||||
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]",
|
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]"
|
||||||
"assets/admin/build/ missing — run: cd assets/admin && npm run build"
|
|
||||||
],
|
],
|
||||||
"info": [
|
"info": [
|
||||||
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ToolsBackup, ToolsHealth, ToolsImportExport",
|
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ExtensionsSecurity, ExtensionsSystem, ToolsBackup, ToolsHealth, ToolsImportExport",
|
||||||
"Menu pages discovered: 60"
|
"Menu pages discovered: 61",
|
||||||
|
"admin build ok; chunks=30"
|
||||||
],
|
],
|
||||||
"domains": {
|
"domains": {
|
||||||
"global": {
|
"global": {
|
||||||
@@ -136,8 +138,7 @@
|
|||||||
"oauth",
|
"oauth",
|
||||||
"report",
|
"report",
|
||||||
"sms",
|
"sms",
|
||||||
"verify",
|
"verify"
|
||||||
"wechat"
|
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"src/Admin/Schema/SchemaUsers.php"
|
"src/Admin/Schema/SchemaUsers.php"
|
||||||
@@ -146,6 +147,7 @@
|
|||||||
"payment": {
|
"payment": {
|
||||||
"sections": [
|
"sections": [
|
||||||
"affiliate",
|
"affiliate",
|
||||||
|
"after_sale",
|
||||||
"gateway",
|
"gateway",
|
||||||
"knowledge",
|
"knowledge",
|
||||||
"recharge",
|
"recharge",
|
||||||
@@ -161,7 +163,9 @@
|
|||||||
"sections": [
|
"sections": [
|
||||||
"audit",
|
"audit",
|
||||||
"ip",
|
"ip",
|
||||||
"push"
|
"push",
|
||||||
|
"security",
|
||||||
|
"system"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"src/Admin/Schema/SchemaExtensions.php"
|
"src/Admin/Schema/SchemaExtensions.php"
|
||||||
@@ -201,6 +205,22 @@
|
|||||||
"src/Admin/Schema/SchemaExtensionsPush.php"
|
"src/Admin/Schema/SchemaExtensionsPush.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ExtensionsSecurity": {
|
||||||
|
"sections": [
|
||||||
|
"security"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"src/Admin/Schema/SchemaExtensionsSecurity.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ExtensionsSystem": {
|
||||||
|
"sections": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"src/Admin/Schema/SchemaExtensionsSystem.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ToolsBackup": {
|
"ToolsBackup": {
|
||||||
"sections": [
|
"sections": [
|
||||||
"backup"
|
"backup"
|
||||||
@@ -226,11 +246,42 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page_count": 60,
|
"page_count": 61,
|
||||||
"build": {
|
"build": {
|
||||||
"present": false,
|
"present": true,
|
||||||
"entry": false,
|
"entry": true,
|
||||||
"missing_chunks": [],
|
"missing_chunks": [],
|
||||||
"chunks": []
|
"chunks": [
|
||||||
|
"SchemaPage.BrFz57tJ.js",
|
||||||
|
"vendor-vue.BqIbVHit.js",
|
||||||
|
"FaIcon.BMA0TNWc.js",
|
||||||
|
"AksMediaField.C9RRDn2V.js",
|
||||||
|
"FaIconPicker.CA3w9r30.js",
|
||||||
|
"shared-api.Cak1GAri.js",
|
||||||
|
"vendor-misc.Dr7TL_YE.js",
|
||||||
|
"vendor-draggable.CjYjXbZD.js",
|
||||||
|
"level.E3ANSDAX.js",
|
||||||
|
"badge.CKTsmH1z.js",
|
||||||
|
"vendor-naive.Dzr6v856.js",
|
||||||
|
"DeferredPage.Bo6-Z82H.js",
|
||||||
|
"NativeOpsRedirect.wMMcdVjx.js",
|
||||||
|
"FloatingButtonsSettings.DM1NXEcr.js",
|
||||||
|
"PageHeader.UhySXCpq.js",
|
||||||
|
"FooterTabbarSettings.8D3P8zNv.js",
|
||||||
|
"PostPageSettings.B5Sm-lcm.js",
|
||||||
|
"CommentSettings.DiQrTo4p.js",
|
||||||
|
"PermissionsCapabilities.CnQfjtxC.js",
|
||||||
|
"CmsCredits.Dh2b9uHb.js",
|
||||||
|
"ThemeInfoPage.zYktV7qz.js",
|
||||||
|
"ThemeAuthorizationPage.e-ksjN6X.js",
|
||||||
|
"DocsPage.Dtd_-3fP.js",
|
||||||
|
"UsersVerify.CAJUfACd.js",
|
||||||
|
"UsersLevel.CBoqr3ux.js",
|
||||||
|
"UsersBadge.B_lwRfdl.js",
|
||||||
|
"UsersWechatTemplate.F0aCC5C-.js",
|
||||||
|
"ToolsImportExport.CN30A6Ge.js",
|
||||||
|
"ToolsHealth.B0GkAdJR.js",
|
||||||
|
"ToolsUpdate.DHtpNLfB.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user