Compare commits
83
Commits
9e3267fef4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a120f61e13 | ||
|
|
0d51beeaad | ||
|
|
573040381d | ||
|
|
7a0445fc4a | ||
|
|
0bc72616e5 | ||
|
|
064f4e0955 | ||
|
|
4b5098a753 | ||
|
|
05f16d5fd7 | ||
|
|
af8dccdc45 | ||
|
|
dcb80cff6e | ||
|
|
2e2558e5ce | ||
|
|
817ac9c647 | ||
|
|
e94ecdd7b1 | ||
|
|
659b77719c | ||
|
|
7f556892ad | ||
|
|
cb76b95b01 | ||
|
|
319ec2f47f | ||
|
|
4b18f7ef2c | ||
|
|
1461c9fe27 | ||
|
|
5641c40958 | ||
|
|
0bee2c605e | ||
|
|
c472045ce4 | ||
|
|
6dc9265260 | ||
|
|
008e63524d | ||
|
|
cff3083a91 | ||
|
|
4d69aaee50 | ||
|
|
ee39024549 | ||
|
|
5810693e01 | ||
|
|
0722c4f00e | ||
|
|
4f2edebab6 | ||
|
|
ce6fbb4e39 | ||
|
|
cf39e72126 | ||
|
|
740bd48f42 | ||
|
|
18e579a8c6 | ||
|
|
cdab7a5e5f | ||
|
|
bd01199930 | ||
|
|
7ed739615e | ||
|
|
60e4d67772 | ||
|
|
7287cfeaec | ||
|
|
f03d72c880 | ||
|
|
6e90a96a15 | ||
|
|
b368678662 | ||
|
|
40b9a3f36b | ||
|
|
6fd80be243 | ||
|
|
855e85e7db | ||
|
|
7e7bf6db23 | ||
|
|
6f815678ec | ||
|
|
8ff74f56ee | ||
|
|
a3b708f9d4 | ||
|
|
9cb5e7bea3 | ||
|
|
16b57dcf1c | ||
|
|
1875470ac7 | ||
|
|
c955fd35f2 | ||
|
|
36b3ba6f45 | ||
|
|
039fa6d863 | ||
|
|
26ce9a5904 | ||
|
|
31e6ba00af | ||
|
|
89ae462610 | ||
|
|
afd08885fc | ||
|
|
536ac67f3c | ||
|
|
7b83893cf6 | ||
|
|
83c164a427 | ||
|
|
5fd8c6fc31 | ||
|
|
b733e41993 | ||
|
|
88669f6989 | ||
|
|
80373746f8 | ||
|
|
d2034f5031 | ||
|
|
ad2290cbb7 | ||
|
|
c870f08395 | ||
|
|
80fb4068d1 | ||
|
|
01cd734989 | ||
|
|
f44180d06c | ||
|
|
c7d18d426c | ||
|
|
0ec405f566 | ||
|
|
dd320c99c1 | ||
|
|
fc75d7b810 | ||
|
|
b3012b3133 | ||
|
|
ab6c2339a0 | ||
|
|
3eb9cc739c | ||
|
|
e9a1a75259 | ||
|
|
d69e1d33c3 | ||
|
|
8023e2e472 | ||
|
|
918ca371be |
No files matched your search
@@ -5,11 +5,4 @@ export const extensionsApiMethods = {
|
|||||||
|
|
||||||
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
||||||
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
||||||
|
|
||||||
/* AI 辅助 */
|
|
||||||
getAiStatus() { return this.request('/ai/status') },
|
|
||||||
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
|
|
||||||
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
|
|
||||||
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
|
|
||||||
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
|
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { renderFaIcon, schemaPage, customPage } from './helpers'
|
import { renderFaIcon, schemaPage } from './helpers'
|
||||||
|
|
||||||
export const extensionsSection = {
|
export const extensionsSection = {
|
||||||
key: 'extensions',
|
key: 'extensions',
|
||||||
@@ -42,7 +42,7 @@ export const extensionsSection = {
|
|||||||
label: '百度推送',
|
label: '百度推送',
|
||||||
route: '/extensions/push',
|
route: '/extensions/push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'push',
|
section: 'push',
|
||||||
}),
|
}),
|
||||||
@@ -56,15 +56,5 @@ export const extensionsSection = {
|
|||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'security',
|
section: 'security',
|
||||||
}),
|
}),
|
||||||
customPage({
|
|
||||||
key: 'extensions-ai',
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
label: 'AI 连接配置',
|
|
||||||
route: '/extensions/ai',
|
|
||||||
component: 'ExtensionAI',
|
|
||||||
status: 'available',
|
|
||||||
title: 'AI 连接配置',
|
|
||||||
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -108,16 +108,6 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'sms',
|
section: 'sms',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'users-wechat',
|
|
||||||
name: 'UsersWechat',
|
|
||||||
label: '微信通知',
|
|
||||||
route: '/users/wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
canonicalDomain: 'users',
|
|
||||||
section: 'wechat',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'users-wechat-template',
|
key: 'users-wechat-template',
|
||||||
name: 'UsersWechatTemplate',
|
name: 'UsersWechatTemplate',
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
|
|
||||||
|
|
||||||
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
|
|
||||||
<strong>加载失败:</strong>{{ loadError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__status">
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
|
||||||
<i class="fa fab fa-android"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">可用状态</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-plug"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">WP AI Client</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-key"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">主题 Provider</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
|
||||||
<i class="fa fas fa-road"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">当前后端</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>主题 Provider 配置(回退链路)</h3>
|
|
||||||
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>AI Provider</label>
|
|
||||||
<select v-model="form.provider" class="aksrt-select">
|
|
||||||
<option value="none">不使用</option>
|
|
||||||
<option value="openai">OpenAI 兼容接口</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>API Key</label>
|
|
||||||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
|
||||||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
|
||||||
<label>API Base URL</label>
|
|
||||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://llm.example.com/v1">
|
|
||||||
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>模型</label>
|
|
||||||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>最大 Token 数</label>
|
|
||||||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>每分钟请求限制</label>
|
|
||||||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
|
||||||
{{ saving ? '保存中...' : '保存设置' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>已接通能力</h3>
|
|
||||||
<table class="aksrt-table aksrt-table--sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>能力</th>
|
|
||||||
<th>入口</th>
|
|
||||||
<th>状态</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="feat in features" :key="feat.key">
|
|
||||||
<td>{{ feat.name }}</td>
|
|
||||||
<td>{{ feat.scene }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
|
||||||
{{ feat.ready ? '可用' : '未就绪' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
|
||||||
<h3><i class="fa fas fa-exclamation-triangle"></i> 重要说明</h3>
|
|
||||||
<ul>
|
|
||||||
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
|
|
||||||
<li>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
|
|
||||||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
|
||||||
import PageHeader from '@/components/PageHeader.vue';
|
|
||||||
import api from '@/api';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
components: { PageHeader },
|
|
||||||
setup() {
|
|
||||||
const status = ref({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
configured: false,
|
|
||||||
model: '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: 10,
|
|
||||||
available: false,
|
|
||||||
backend: 'none',
|
|
||||||
wp_ai_available: false,
|
|
||||||
theme_available: false,
|
|
||||||
seo_ai_enabled: false,
|
|
||||||
write_assist: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
api_key: '',
|
|
||||||
api_base: 'https://api.openai.com/v1',
|
|
||||||
model: 'gpt-3.5-turbo',
|
|
||||||
max_tokens: 1000,
|
|
||||||
rate_limit: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const saving = ref(false);
|
|
||||||
const loadError = ref('');
|
|
||||||
|
|
||||||
const backendLabel = computed(() => {
|
|
||||||
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
|
|
||||||
return map[status.value.backend] || status.value.backend || '无';
|
|
||||||
});
|
|
||||||
|
|
||||||
const features = computed(() => {
|
|
||||||
const avail = !!status.value.available;
|
|
||||||
const seoReady = avail && !!status.value.seo_ai_enabled;
|
|
||||||
return [
|
|
||||||
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
|
|
||||||
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
|
|
||||||
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function hydrateFromStatus(data) {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
status.value = {
|
|
||||||
enabled: !!data.enabled,
|
|
||||||
provider: data.provider || 'none',
|
|
||||||
configured: !!data.configured,
|
|
||||||
model: data.model || '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: data.rate_limit || 10,
|
|
||||||
available: !!data.available,
|
|
||||||
backend: data.backend || 'none',
|
|
||||||
wp_ai_available: !!data.wp_ai_available,
|
|
||||||
theme_available: !!data.theme_available,
|
|
||||||
seo_ai_enabled: !!data.seo_ai_enabled,
|
|
||||||
write_assist: !!data.write_assist,
|
|
||||||
};
|
|
||||||
form.enabled = !!data.enabled;
|
|
||||||
form.provider = data.provider || 'none';
|
|
||||||
form.model = data.model || 'gpt-3.5-turbo';
|
|
||||||
form.rate_limit = data.rate_limit || 10;
|
|
||||||
if (data.api_base) form.api_base = data.api_base;
|
|
||||||
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
|
||||||
form.max_tokens = data.max_tokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStatus() {
|
|
||||||
loadError.value = '';
|
|
||||||
try {
|
|
||||||
const json = await api.getAiStatus();
|
|
||||||
hydrateFromStatus(json.data || {});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Load AI status failed:', e);
|
|
||||||
loadError.value = e?.message || '加载 AI 状态失败';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSettings() {
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
enabled: form.enabled,
|
|
||||||
provider: form.provider,
|
|
||||||
api_base: form.api_base,
|
|
||||||
model: form.model,
|
|
||||||
max_tokens: form.max_tokens,
|
|
||||||
rate_limit: form.rate_limit,
|
|
||||||
};
|
|
||||||
// Only send api_key when the user entered a new one.
|
|
||||||
if (form.api_key && form.api_key !== '***') {
|
|
||||||
payload.api_key = form.api_key;
|
|
||||||
}
|
|
||||||
const json = await api.updateAiSettings(payload);
|
|
||||||
alert(json.message || '设置已保存');
|
|
||||||
await loadStatus();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Save failed:', e);
|
|
||||||
alert(e?.message || '保存失败');
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadStatus());
|
|
||||||
|
|
||||||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-ai__status {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
|
|
||||||
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
|
|
||||||
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: var(--aksrt-color-text); }
|
|
||||||
.aksrt-ai__status-label { font-size: 12px; color: #888; }
|
|
||||||
.aksrt-ai__form-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
|
|
||||||
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
|
||||||
.aksrt-ai__load-error {
|
|
||||||
background: #fdecea;
|
|
||||||
border-color: #f5c2c0;
|
|
||||||
color: #842029;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
|
|
||||||
.aksrt-ai__notice h3 { color: #e65100; }
|
|
||||||
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
|
|
||||||
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
|
|
||||||
.aksrt-ai__notice code {
|
|
||||||
background: rgba(0,0,0,0.06);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
|
|
||||||
.aksrt-badge--success { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
|
||||||
.aksrt-form-group { margin-bottom: 14px; }
|
|
||||||
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
|
|
||||||
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
|
|
||||||
.aksrt-button--primary { background: #1677ff; color: #fff; }
|
|
||||||
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
|
|
||||||
</style>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const extensionsPageLoaders = {
|
|
||||||
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { corePageLoaders } from './core'
|
import { corePageLoaders } from './core'
|
||||||
import { usersPageLoaders } from './users'
|
import { usersPageLoaders } from './users'
|
||||||
import { toolsPageLoaders } from './tools'
|
import { toolsPageLoaders } from './tools'
|
||||||
import { extensionsPageLoaders } from './extensions'
|
|
||||||
|
|
||||||
export const componentLoaders = {
|
export const componentLoaders = {
|
||||||
...corePageLoaders,
|
...corePageLoaders,
|
||||||
...usersPageLoaders,
|
...usersPageLoaders,
|
||||||
...toolsPageLoaders,
|
...toolsPageLoaders,
|
||||||
...extensionsPageLoaders,
|
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,16 @@ export const pushSchema = {
|
|||||||
key: 'extensions.push',
|
key: 'extensions.push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
icon: 'fa fas fa-paper-plane',
|
icon: 'fa fas fa-paper-plane',
|
||||||
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
description: '配置百度主动推送、收录通道和推送状态。',
|
||||||
defaults: {
|
defaults: {
|
||||||
baidu_push_enabled: false,
|
baidu_push_enabled: false,
|
||||||
baidu_push_token: '',
|
baidu_push_token: '',
|
||||||
baidu_push_auto: true,
|
baidu_push_auto: true,
|
||||||
baidu_push_on_publish: true,
|
baidu_push_on_publish: true,
|
||||||
baidu_push_on_update: false,
|
baidu_push_on_update: false,
|
||||||
baidu_push_batch_size: 50,
|
baidu_push_normal: true,
|
||||||
|
baidu_push_fast: false,
|
||||||
baidu_push_log_enabled: true,
|
baidu_push_log_enabled: true,
|
||||||
xzh_on: false,
|
|
||||||
xzh_appid: '',
|
|
||||||
xzh_render_tail: true,
|
|
||||||
xzh_jsonld_single: true,
|
|
||||||
xzh_jsonld_page: false,
|
|
||||||
xzh_jsonld_img: false,
|
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
@@ -25,7 +20,7 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-rocket',
|
icon: 'fa fas fa-rocket',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
||||||
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' },
|
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,21 +29,17 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-bolt',
|
icon: 'fa fas fa-bolt',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
||||||
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' },
|
||||||
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'xzh',
|
key: 'channel',
|
||||||
title: '熊掌号 / JSON-LD',
|
title: '收录通道',
|
||||||
icon: 'fa fas fa-paw',
|
icon: 'fa fas fa-inbox',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
|
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
|
||||||
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
|
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
|
||||||
{ key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,8 +47,7 @@ export const pushSchema = {
|
|||||||
title: '高级设置',
|
title: '高级设置',
|
||||||
icon: 'fa fas fa-cog',
|
icon: 'fa fas fa-cog',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
|
||||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const seoSchema = {
|
|||||||
key: 'global.seo',
|
key: 'global.seo',
|
||||||
title: 'SEO 优化',
|
title: 'SEO 优化',
|
||||||
icon: 'fa fas fa-search',
|
icon: 'fa fas fa-search',
|
||||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框;连接配置在「扩展 → AI 连接配置」。优先使用 WordPress AI Connectors。',
|
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框,连接由 WordPress AI Connectors 提供。',
|
||||||
defaults: {
|
defaults: {
|
||||||
seo_enabled: true,
|
seo_enabled: true,
|
||||||
seo_ai_enabled: false,
|
seo_ai_enabled: false,
|
||||||
@@ -13,10 +13,6 @@ export const seoSchema = {
|
|||||||
ai_seo_title_prompt: '',
|
ai_seo_title_prompt: '',
|
||||||
ai_seo_keywords_prompt: '',
|
ai_seo_keywords_prompt: '',
|
||||||
ai_seo_description_prompt: '',
|
ai_seo_description_prompt: '',
|
||||||
seo_baidu_push: false,
|
|
||||||
seo_baidu_normal: false,
|
|
||||||
seo_baidu_fast: false,
|
|
||||||
seo_baidu_token: '',
|
|
||||||
seo_image_optimize: true,
|
seo_image_optimize: true,
|
||||||
seo_image_default: '',
|
seo_image_default: '',
|
||||||
seo_image_always_default: false,
|
seo_image_always_default: false,
|
||||||
@@ -48,7 +44,7 @@ export const seoSchema = {
|
|||||||
label: '启用 AI SEO 生成',
|
label: '启用 AI SEO 生成',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
default: false,
|
default: false,
|
||||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。',
|
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先在 WordPress 配置 AI Connectors。',
|
||||||
dependsOn: { key: 'seo_enabled', value: true },
|
dependsOn: { key: 'seo_enabled', value: true },
|
||||||
},
|
},
|
||||||
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
||||||
@@ -95,17 +91,6 @@ export const seoSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'baidu',
|
|
||||||
title: '百度推送',
|
|
||||||
icon: 'fa fas fa-paper-plane',
|
|
||||||
fields: [
|
|
||||||
{ key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'image',
|
key: 'image',
|
||||||
title: '图片 SEO',
|
title: '图片 SEO',
|
||||||
|
|||||||
@@ -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', sanitize_type: 'html', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '支持HTML,请注意控制长度' },
|
||||||
|
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', sanitize_type: 'html', default: '尊享专属特权', dependsOn: dep, help: '支持HTML' },
|
||||||
|
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '支持安全HTML,建议使用 <li> 列表项' },
|
||||||
|
{ key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' },
|
||||||
|
{
|
||||||
|
key: `vip_${level}_product`,
|
||||||
|
label: '会员商品',
|
||||||
|
type: 'group',
|
||||||
|
max: 8,
|
||||||
|
button_title: '添加会员商品',
|
||||||
|
default: [
|
||||||
|
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
|
...vipProductGroupOptions,
|
||||||
|
dependsOn: { all: [dep, { key: `vip_${level}_product_s`, value: true }] },
|
||||||
|
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||||
|
fields: vipProductFields(),
|
||||||
|
},
|
||||||
|
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export const vipSchema = {
|
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,111 @@ 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',
|
||||||
|
sanitize_type: 'html',
|
||||||
|
default: '立即续费会员,畅享VIP权益',
|
||||||
|
dependsOn: { key: 'vip_renew', value: true },
|
||||||
|
help: '续费会员的一句话简介,支持HTML',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
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 +372,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', sanitize_type: 'html', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介,支持HTML' },
|
||||||
{
|
{
|
||||||
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 +428,72 @@ 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',
|
||||||
|
sanitize_type: 'html',
|
||||||
|
default: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||||
|
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||||
|
help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍,支持HTML',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pay_user_vip_more',
|
||||||
|
label: '开通会员更多内容',
|
||||||
|
type: 'textarea',
|
||||||
|
sanitize_type: 'html',
|
||||||
|
default: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||||
|
rows: 4,
|
||||||
|
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||||
|
help: '显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码',
|
||||||
},
|
},
|
||||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export const permissionsSchemas = {
|
|||||||
{ label: '每日签到', value: 'signin' },
|
{ label: '每日签到', value: 'signin' },
|
||||||
{ label: '前台投稿', value: 'front_submit' },
|
{ label: '前台投稿', value: 'front_submit' },
|
||||||
{ label: '打赏用户', value: 'tip_user' },
|
{ label: '打赏用户', value: 'tip_user' },
|
||||||
|
{ label: '申请售后', value: 'after_sale_apply' },
|
||||||
|
{ label: '撤销售后', value: 'after_sale_cancel' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
+237
-359
@@ -916,6 +916,7 @@ strong, b {
|
|||||||
|
|
||||||
/* === Full width === */
|
/* === Full width === */
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,6 +962,8 @@ strong, b {
|
|||||||
.but > .fa:not(.fa-angle-right),
|
.but > .fa:not(.fa-angle-right),
|
||||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||||
|
|
||||||
|
.but.block { display: block; }
|
||||||
|
|
||||||
/* hover / active */
|
/* hover / active */
|
||||||
.but:focus,
|
.but:focus,
|
||||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||||
@@ -1092,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%); }
|
||||||
@@ -2062,6 +2065,22 @@ html.dark .alert--info { color: #93C5FD; }
|
|||||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 8b. 头像形状统一 — 圆形基底
|
||||||
|
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||||
|
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||||
|
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||||
|
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||||
|
* ============================================================ */
|
||||||
|
img.avatar,
|
||||||
|
img.avatar-mini,
|
||||||
|
img.avatar-md,
|
||||||
|
img.avatar-lg,
|
||||||
|
.avatar-img img {
|
||||||
|
border-radius: var(--radius-avatar, 50%);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||||
@@ -2638,7 +2657,8 @@ main.container,
|
|||||||
.user-card.author .user-name > name {
|
.user-card.author .user-name > name {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.user-card.author .user-name > name > .display-name {
|
.user-card.author .user-name > name > .display-name,
|
||||||
|
.user-card.widget .user-name > name > .display-name {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -2688,31 +2708,35 @@ main.container,
|
|||||||
/**
|
/**
|
||||||
* Header account popover.
|
* Header account popover.
|
||||||
*
|
*
|
||||||
* The shared legacy navigation styles provide the popover shell. This module
|
* The shared legacy navigation styles provide the popover shell, and every
|
||||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||||
|
* through the reference legacy classes. This module only owns the navigation
|
||||||
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||||
.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;
|
||||||
}
|
}
|
||||||
@@ -2725,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;
|
||||||
@@ -2790,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). */
|
||||||
@@ -2852,85 +2844,41 @@ main.container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox {
|
.aksrt-header-userbox {
|
||||||
width: 260px;
|
|
||||||
color: var(--color-ink-2);
|
color: var(--color-ink-2);
|
||||||
|
line-height: 1.42857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info {
|
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||||
min-height: 44px;
|
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||||
padding: 2px;
|
.aksrt-header-userbox .muted-box.padding-6 {
|
||||||
|
background-color: var(--this-bg, var(--color-line-soft));
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .avatar {
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
flex: 0 0 44px;
|
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||||
width: 44px;
|
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||||
height: 44px;
|
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||||
object-fit: cover;
|
* declarations are the last word in the cascade. Re-assert them here, which
|
||||||
|
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||||
|
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||||
|
.payvip-icon {
|
||||||
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
|
color: #ece0e0;
|
||||||
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right,
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
.aksrt-header-userbox .user-right > .flex1 {
|
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||||
min-width: 0;
|
display: block;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
.aksrt-header-userbox .user-right b {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .display-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right .img-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .abs-right {
|
|
||||||
top: 50%;
|
|
||||||
right: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .aksrt-notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
right: -4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||||
.aksrt-nav-notification .aksrt-notification-badge {
|
.aksrt-nav-notification .aksrt-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@@ -2950,222 +2898,20 @@ main.container,
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-wallet {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet:hover {
|
|
||||||
color: var(--color-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item i {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item b {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: #fff6db !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: #b78103 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin:hover {
|
|
||||||
background: #fff0c2 !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
|
||||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
|
||||||
.aksrt-header-checkin:disabled,
|
|
||||||
.aksrt-header-checkin:disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
opacity: .92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 7px 10px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__exp {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
box-shadow: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
opacity: .9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||||
* icon tiles and must opt out or their labels overlap the following row. */
|
* icon tiles and must opt out or their labels overlap the following row.
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
* The tile geometry itself stays with the shared legacy rules
|
||||||
align-items: flex-start;
|
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||||
gap: 4px;
|
* so the action row keeps the reference proportions. */
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||||
.aksrt-header-userbox .header-user-href > a {
|
.aksrt-header-userbox .header-user-href > a {
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 10px 2px 2px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex: 0 0 36px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-navbar .aksrt-header-userbox {
|
.mobile-navbar .aksrt-header-userbox {
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin-right: auto;
|
margin: auto;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin {
|
|
||||||
background: rgba(255, 193, 7, .18) !important;
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin > i {
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet {
|
|
||||||
background: rgba(255, 255, 255, .06);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet__item,
|
|
||||||
body.dark-theme .aksrt-header-wallet__item b {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.aksrt-header-userbox .header-user-href {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex-basis: 34px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* source: assets/css/frontend/card-primitives.css */
|
/* source: assets/css/frontend/card-primitives.css */
|
||||||
@@ -6496,7 +6242,8 @@ body.aksrt-modal-runtime-open {
|
|||||||
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
|
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
|
||||||
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
||||||
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
||||||
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
||||||
|
.aksrt-after-sale-withdraw-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||||
width: min(400px, calc(100vw - 32px));
|
width: min(400px, calc(100vw - 32px));
|
||||||
border-radius: var(--main-radius, 8px);
|
border-radius: var(--main-radius, 8px);
|
||||||
background: var(--main-bg-color, #fff);
|
background: var(--main-bg-color, #fff);
|
||||||
@@ -6504,7 +6251,8 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
|
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
|
||||||
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
|
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
|
||||||
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton {
|
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton,
|
||||||
|
.aksrt-after-sale-withdraw-modal .aksrt-modal-runtime__skeleton {
|
||||||
padding: 24px 18px;
|
padding: 24px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6513,9 +6261,13 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header,
|
||||||
|
.aksrt-order-detail-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-apply-modal .colorful-bg,
|
.aksrt-after-sale-apply-modal .colorful-bg,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg,
|
.aksrt-after-sale-detail-modal .colorful-bg,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg {
|
.aksrt-after-sale-expired-modal .colorful-bg,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--this-color);
|
color: var(--this-color);
|
||||||
@@ -6525,7 +6277,9 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header {
|
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header,
|
||||||
|
.aksrt-order-detail-modal .modal-colorful-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -6539,13 +6293,17 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
|
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
|
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header + * {
|
.aksrt-after-sale-expired-modal .modal-colorful-header + *,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header + *,
|
||||||
|
.aksrt-order-detail-modal .modal-colorful-header + * {
|
||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
|
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
|
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header > .close {
|
.aksrt-after-sale-expired-modal .modal-colorful-header > .close,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header > .close,
|
||||||
|
.aksrt-order-detail-modal .modal-colorful-header > .close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@@ -6562,8 +6320,14 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after {
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: var(--this-color);
|
color: var(--this-color);
|
||||||
background: var(--this-bg);
|
background: var(--this-bg);
|
||||||
@@ -6573,7 +6337,9 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make {
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make {
|
||||||
top: 60%;
|
top: 60%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 323px;
|
width: 323px;
|
||||||
@@ -6583,7 +6349,9 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before {
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before {
|
||||||
top: -325px;
|
top: -325px;
|
||||||
right: -558px;
|
right: -558px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -6595,7 +6363,9 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after {
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
||||||
top: -170px;
|
top: -170px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: -262px;
|
left: -262px;
|
||||||
@@ -6669,6 +6439,114 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-status.is-rejected,
|
.aksrt-after-sale-status.is-rejected,
|
||||||
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
||||||
|
|
||||||
|
/* 售后处理记录时间线:[节点][处理状态][时间] 一行,处理备注另起一行并对齐状态文字。
|
||||||
|
售后详细弹窗与订单详情弹窗的「售后记录」共用同一套标记;竖线为条目的
|
||||||
|
::before,末项不收口,避免出现悬空的线段。 */
|
||||||
|
.aksrt-after-sale-log { display: grid; gap: 6px; }
|
||||||
|
.aksrt-after-sale-log__empty { color: var(--modal-muted); font-size: 12px; }
|
||||||
|
.aksrt-after-sale-timeline { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||||
|
.aksrt-after-sale-timeline__item { --as-accent: var(--modal-muted); position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; padding: 0 0 14px; }
|
||||||
|
.aksrt-after-sale-timeline__item:last-child { padding-bottom: 0; }
|
||||||
|
.aksrt-after-sale-timeline__item.is-pending { --as-accent: var(--modal-warning); }
|
||||||
|
.aksrt-after-sale-timeline__item.is-processing { --as-accent: var(--modal-primary); }
|
||||||
|
.aksrt-after-sale-timeline__item.is-completed { --as-accent: var(--modal-success); }
|
||||||
|
.aksrt-after-sale-timeline__item.is-rejected,
|
||||||
|
.aksrt-after-sale-timeline__item.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||||
|
.aksrt-after-sale-timeline__item::before { content: ""; position: absolute; top: 14px; bottom: -2px; left: 4px; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--as-accent) 30%, var(--modal-line)); }
|
||||||
|
.aksrt-after-sale-timeline__item:last-child::before { display: none; }
|
||||||
|
.aksrt-after-sale-timeline__knot { position: relative; z-index: 1; width: 10px; height: 10px; margin-top: 4px; border: 2px solid var(--as-accent); border-radius: 50%; background: var(--modal-surface, #fff); }
|
||||||
|
.aksrt-after-sale-timeline__body { display: grid; gap: 3px; min-width: 0; }
|
||||||
|
.aksrt-after-sale-timeline__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
||||||
|
.aksrt-after-sale-timeline__status { color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||||
|
.aksrt-after-sale-timeline__time { color: var(--modal-muted); font-size: 11px; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
||||||
|
.aksrt-after-sale-timeline__note { margin: 0; color: var(--modal-ink); font-size: 12.5px; line-height: 1.65; word-break: break-word; }
|
||||||
|
|
||||||
|
/* 状态行右侧单元格:状态徽章 + 「撤销售后」入口(仅待审核工单渲染)。 */
|
||||||
|
.aksrt-after-sale-detail__statusCell { display: inline-flex; align-items: center; gap: 10px; }
|
||||||
|
.aksrt-after-sale-withdraw { display: inline-flex; align-items: center; gap: 3px; padding: 0; border: 0; background: transparent; color: var(--modal-danger, #ef4444); font-size: 12px; font-weight: 600; line-height: 1.4; cursor: pointer; transition: color .15s ease; }
|
||||||
|
.aksrt-after-sale-withdraw .fa { font-size: 12px; }
|
||||||
|
.aksrt-after-sale-withdraw:hover { color: color-mix(in srgb, var(--modal-danger) 78%, var(--modal-ink)); }
|
||||||
|
|
||||||
|
/* 售后提示弹窗(撤销售后确认)正文。 */
|
||||||
|
.aksrt-after-sale-withdraw__body { margin-bottom: 16px; color: var(--color-ink-2, #4e5358); line-height: 1.7; }
|
||||||
|
|
||||||
|
/* 售后弹窗底部按钮:全宽平铺贴底(负 margin 抵消 __content 内边距)+ 分隔线,
|
||||||
|
子按钮去独立背景/边框/圆角(文字色区分主次),覆盖上方全局按钮网格的胶囊观感;
|
||||||
|
仅在售后弹窗家族生效,其他 runtime 弹窗仍走全局网格样式。
|
||||||
|
footer 自身不留垂直内边距——上下 12px 并入按钮(min-height 62 = 12+38+12),
|
||||||
|
按钮盒即整个底部区域(hover 底色/分界线随之满幅)。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts {
|
||||||
|
gap: 0;
|
||||||
|
margin: 16px -18px -16px;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid var(--modal-line-soft, rgba(50, 50, 50, 0.06));
|
||||||
|
background: var(--main-bg-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but {
|
||||||
|
min-height: 62px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--modal-muted, #777);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
--this-bg: transparent;
|
||||||
|
--this-border: transparent;
|
||||||
|
--this-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but.c-blue,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but.c-blue,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but.c-blue,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but.c-blue {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--modal-primary, #f04494);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相邻按钮分界线:legacy footer 自带 .but + .but 左分隔线,上方平铺规则的 border:0
|
||||||
|
会把它抹掉,这里按家族分隔色还原(单按钮无相邻兄弟,天然无分界线)。
|
||||||
|
需排在 .c-blue 规则之后:确认提交等主按钮同受 border:0 影响,后者同名优先级。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but + .but,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but + .but,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but + .but,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but + .but {
|
||||||
|
border-left: 1px solid var(--modal-line-soft, rgba(50, 50, 50, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停反馈(参考 Element 卡片 footer 的 hover 语言):整块区域浅灰底 + 文字转主色,
|
||||||
|
次级按钮 hover 升为主色、主按钮略加深;平铺按钮已满幅,底色即铺满整个区域,
|
||||||
|
基础 .but 的 0.15s 过渡自动生效,并压掉 compat 层的 hover 半透明。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but:hover,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but:hover,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but:hover,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: color-mix(in srgb, var(--modal-ink, #333333) 5%, transparent);
|
||||||
|
color: var(--modal-primary, #f04494);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but.c-blue:hover,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but.c-blue:hover,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but.c-blue:hover,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but.c-blue:hover {
|
||||||
|
color: color-mix(in srgb, var(--modal-primary, #f04494) 82%, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单按钮 footer 占满整行(全局网格是 1fr 1fr,单按钮会只占一半)。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but:only-child,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but:only-child,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but:only-child,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but:only-child { grid-column: 1 / -1; }
|
||||||
|
|
||||||
/* source: assets/css/frontend/notyf.css */
|
/* source: assets/css/frontend/notyf.css */
|
||||||
/**
|
/**
|
||||||
* legacy-compatible frontend notifications.
|
* legacy-compatible frontend notifications.
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
|
|
||||||
/* === Full width === */
|
/* === Full width === */
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +183,8 @@
|
|||||||
.but > .fa:not(.fa-angle-right),
|
.but > .fa:not(.fa-angle-right),
|
||||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||||
|
|
||||||
|
.but.block { display: block; }
|
||||||
|
|
||||||
/* hover / active */
|
/* hover / active */
|
||||||
.but:focus,
|
.but:focus,
|
||||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||||
@@ -313,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%); }
|
||||||
|
|||||||
@@ -424,6 +424,22 @@
|
|||||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 8b. 头像形状统一 — 圆形基底
|
||||||
|
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||||
|
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||||
|
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||||
|
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||||
|
* ============================================================ */
|
||||||
|
img.avatar,
|
||||||
|
img.avatar-mini,
|
||||||
|
img.avatar-md,
|
||||||
|
img.avatar-lg,
|
||||||
|
.avatar-img img {
|
||||||
|
border-radius: var(--radius-avatar, 50%);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ main.container,
|
|||||||
.user-card.author .user-name > name {
|
.user-card.author .user-name > name {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.user-card.author .user-name > name > .display-name {
|
.user-card.author .user-name > name > .display-name,
|
||||||
|
.user-card.widget .user-name > name > .display-name {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* Header account popover.
|
* Header account popover.
|
||||||
*
|
*
|
||||||
* The shared legacy navigation styles provide the popover shell. This module
|
* The shared legacy navigation styles provide the popover shell, and every
|
||||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||||
|
* through the reference legacy classes. This module only owns the navigation
|
||||||
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||||
.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;
|
||||||
}
|
}
|
||||||
@@ -38,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;
|
||||||
@@ -103,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). */
|
||||||
@@ -165,85 +137,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox {
|
.aksrt-header-userbox {
|
||||||
width: 260px;
|
|
||||||
color: var(--color-ink-2);
|
color: var(--color-ink-2);
|
||||||
|
line-height: 1.42857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info {
|
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||||
min-height: 44px;
|
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||||
padding: 2px;
|
.aksrt-header-userbox .muted-box.padding-6 {
|
||||||
|
background-color: var(--this-bg, var(--color-line-soft));
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .avatar {
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
flex: 0 0 44px;
|
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||||
width: 44px;
|
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||||
height: 44px;
|
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||||
object-fit: cover;
|
* declarations are the last word in the cascade. Re-assert them here, which
|
||||||
|
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||||
|
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||||
|
.payvip-icon {
|
||||||
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
|
color: #ece0e0;
|
||||||
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right,
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
.aksrt-header-userbox .user-right > .flex1 {
|
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||||
min-width: 0;
|
display: block;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
.aksrt-header-userbox .user-right b {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .display-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right .img-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .abs-right {
|
|
||||||
top: 50%;
|
|
||||||
right: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .aksrt-notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
right: -4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||||
.aksrt-nav-notification .aksrt-notification-badge {
|
.aksrt-nav-notification .aksrt-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@@ -263,220 +191,18 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-wallet {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet:hover {
|
|
||||||
color: var(--color-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item i {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item b {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: #fff6db !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: #b78103 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin:hover {
|
|
||||||
background: #fff0c2 !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
|
||||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
|
||||||
.aksrt-header-checkin:disabled,
|
|
||||||
.aksrt-header-checkin:disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
opacity: .92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 7px 10px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__exp {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
box-shadow: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
opacity: .9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||||
* icon tiles and must opt out or their labels overlap the following row. */
|
* icon tiles and must opt out or their labels overlap the following row.
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
* The tile geometry itself stays with the shared legacy rules
|
||||||
align-items: flex-start;
|
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||||
gap: 4px;
|
* so the action row keeps the reference proportions. */
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||||
.aksrt-header-userbox .header-user-href > a {
|
.aksrt-header-userbox .header-user-href > a {
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 10px 2px 2px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex: 0 0 36px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-navbar .aksrt-header-userbox {
|
.mobile-navbar .aksrt-header-userbox {
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin-right: auto;
|
margin: auto;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin {
|
|
||||||
background: rgba(255, 193, 7, .18) !important;
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin > i {
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet {
|
|
||||||
background: rgba(255, 255, 255, .06);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet__item,
|
|
||||||
body.dark-theme .aksrt-header-wallet__item b {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.aksrt-header-userbox .header-user-href {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex-basis: 34px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -619,7 +619,8 @@ body.aksrt-modal-runtime-open {
|
|||||||
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
|
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
|
||||||
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
.aksrt-after-sale-apply-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
||||||
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
.aksrt-after-sale-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
||||||
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
.aksrt-after-sale-expired-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog,
|
||||||
|
.aksrt-after-sale-withdraw-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||||
width: min(400px, calc(100vw - 32px));
|
width: min(400px, calc(100vw - 32px));
|
||||||
border-radius: var(--main-radius, 8px);
|
border-radius: var(--main-radius, 8px);
|
||||||
background: var(--main-bg-color, #fff);
|
background: var(--main-bg-color, #fff);
|
||||||
@@ -627,7 +628,8 @@ body.aksrt-modal-runtime-open {
|
|||||||
|
|
||||||
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
|
.aksrt-after-sale-apply-modal .aksrt-modal-runtime__skeleton,
|
||||||
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
|
.aksrt-after-sale-detail-modal .aksrt-modal-runtime__skeleton,
|
||||||
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton {
|
.aksrt-after-sale-expired-modal .aksrt-modal-runtime__skeleton,
|
||||||
|
.aksrt-after-sale-withdraw-modal .aksrt-modal-runtime__skeleton {
|
||||||
padding: 24px 18px;
|
padding: 24px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,10 +638,12 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header,
|
||||||
.aksrt-order-detail-modal .modal-colorful-header,
|
.aksrt-order-detail-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-apply-modal .colorful-bg,
|
.aksrt-after-sale-apply-modal .colorful-bg,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg,
|
.aksrt-after-sale-detail-modal .colorful-bg,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg,
|
.aksrt-after-sale-expired-modal .colorful-bg,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg,
|
||||||
.aksrt-order-detail-modal .colorful-bg {
|
.aksrt-order-detail-modal .colorful-bg {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -651,6 +655,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
.aksrt-after-sale-detail-modal .modal-colorful-header,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
.aksrt-after-sale-expired-modal .modal-colorful-header,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header,
|
||||||
.aksrt-order-detail-modal .modal-colorful-header {
|
.aksrt-order-detail-modal .modal-colorful-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -666,6 +671,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
|
.aksrt-after-sale-apply-modal .modal-colorful-header + *,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
|
.aksrt-after-sale-detail-modal .modal-colorful-header + *,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header + *,
|
.aksrt-after-sale-expired-modal .modal-colorful-header + *,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header + *,
|
||||||
.aksrt-order-detail-modal .modal-colorful-header + * {
|
.aksrt-order-detail-modal .modal-colorful-header + * {
|
||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
}
|
}
|
||||||
@@ -673,6 +679,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
|
.aksrt-after-sale-apply-modal .modal-colorful-header > .close,
|
||||||
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
|
.aksrt-after-sale-detail-modal .modal-colorful-header > .close,
|
||||||
.aksrt-after-sale-expired-modal .modal-colorful-header > .close,
|
.aksrt-after-sale-expired-modal .modal-colorful-header > .close,
|
||||||
|
.aksrt-after-sale-withdraw-modal .modal-colorful-header > .close,
|
||||||
.aksrt-order-detail-modal .modal-colorful-header > .close {
|
.aksrt-order-detail-modal .modal-colorful-header > .close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@@ -690,10 +697,13 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-order-detail-modal .colorful-bg > .colorful-make,
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before,
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: var(--this-color);
|
color: var(--this-color);
|
||||||
@@ -705,6 +715,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make,
|
||||||
.aksrt-order-detail-modal .colorful-bg > .colorful-make {
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make {
|
||||||
top: 60%;
|
top: 60%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -716,6 +727,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::before,
|
||||||
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before {
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before {
|
||||||
top: -325px;
|
top: -325px;
|
||||||
right: -558px;
|
right: -558px;
|
||||||
@@ -729,6 +741,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
|
.aksrt-after-sale-expired-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-after-sale-withdraw-modal .colorful-bg > .colorful-make::after,
|
||||||
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
||||||
top: -170px;
|
top: -170px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -802,3 +815,111 @@ body.aksrt-modal-runtime-open {
|
|||||||
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
|
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
|
||||||
.aksrt-after-sale-status.is-rejected,
|
.aksrt-after-sale-status.is-rejected,
|
||||||
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
||||||
|
|
||||||
|
/* 售后处理记录时间线:[节点][处理状态][时间] 一行,处理备注另起一行并对齐状态文字。
|
||||||
|
售后详细弹窗与订单详情弹窗的「售后记录」共用同一套标记;竖线为条目的
|
||||||
|
::before,末项不收口,避免出现悬空的线段。 */
|
||||||
|
.aksrt-after-sale-log { display: grid; gap: 6px; }
|
||||||
|
.aksrt-after-sale-log__empty { color: var(--modal-muted); font-size: 12px; }
|
||||||
|
.aksrt-after-sale-timeline { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||||
|
.aksrt-after-sale-timeline__item { --as-accent: var(--modal-muted); position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; padding: 0 0 14px; }
|
||||||
|
.aksrt-after-sale-timeline__item:last-child { padding-bottom: 0; }
|
||||||
|
.aksrt-after-sale-timeline__item.is-pending { --as-accent: var(--modal-warning); }
|
||||||
|
.aksrt-after-sale-timeline__item.is-processing { --as-accent: var(--modal-primary); }
|
||||||
|
.aksrt-after-sale-timeline__item.is-completed { --as-accent: var(--modal-success); }
|
||||||
|
.aksrt-after-sale-timeline__item.is-rejected,
|
||||||
|
.aksrt-after-sale-timeline__item.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||||
|
.aksrt-after-sale-timeline__item::before { content: ""; position: absolute; top: 14px; bottom: -2px; left: 4px; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--as-accent) 30%, var(--modal-line)); }
|
||||||
|
.aksrt-after-sale-timeline__item:last-child::before { display: none; }
|
||||||
|
.aksrt-after-sale-timeline__knot { position: relative; z-index: 1; width: 10px; height: 10px; margin-top: 4px; border: 2px solid var(--as-accent); border-radius: 50%; background: var(--modal-surface, #fff); }
|
||||||
|
.aksrt-after-sale-timeline__body { display: grid; gap: 3px; min-width: 0; }
|
||||||
|
.aksrt-after-sale-timeline__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
||||||
|
.aksrt-after-sale-timeline__status { color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||||
|
.aksrt-after-sale-timeline__time { color: var(--modal-muted); font-size: 11px; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
||||||
|
.aksrt-after-sale-timeline__note { margin: 0; color: var(--modal-ink); font-size: 12.5px; line-height: 1.65; word-break: break-word; }
|
||||||
|
|
||||||
|
/* 状态行右侧单元格:状态徽章 + 「撤销售后」入口(仅待审核工单渲染)。 */
|
||||||
|
.aksrt-after-sale-detail__statusCell { display: inline-flex; align-items: center; gap: 10px; }
|
||||||
|
.aksrt-after-sale-withdraw { display: inline-flex; align-items: center; gap: 3px; padding: 0; border: 0; background: transparent; color: var(--modal-danger, #ef4444); font-size: 12px; font-weight: 600; line-height: 1.4; cursor: pointer; transition: color .15s ease; }
|
||||||
|
.aksrt-after-sale-withdraw .fa { font-size: 12px; }
|
||||||
|
.aksrt-after-sale-withdraw:hover { color: color-mix(in srgb, var(--modal-danger) 78%, var(--modal-ink)); }
|
||||||
|
|
||||||
|
/* 售后提示弹窗(撤销售后确认)正文。 */
|
||||||
|
.aksrt-after-sale-withdraw__body { margin-bottom: 16px; color: var(--color-ink-2, #4e5358); line-height: 1.7; }
|
||||||
|
|
||||||
|
/* 售后弹窗底部按钮:全宽平铺贴底(负 margin 抵消 __content 内边距)+ 分隔线,
|
||||||
|
子按钮去独立背景/边框/圆角(文字色区分主次),覆盖上方全局按钮网格的胶囊观感;
|
||||||
|
仅在售后弹窗家族生效,其他 runtime 弹窗仍走全局网格样式。
|
||||||
|
footer 自身不留垂直内边距——上下 12px 并入按钮(min-height 62 = 12+38+12),
|
||||||
|
按钮盒即整个底部区域(hover 底色/分界线随之满幅)。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts {
|
||||||
|
gap: 0;
|
||||||
|
margin: 16px -18px -16px;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid var(--modal-line-soft, rgba(50, 50, 50, 0.06));
|
||||||
|
background: var(--main-bg-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but {
|
||||||
|
min-height: 62px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--modal-muted, #777);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
--this-bg: transparent;
|
||||||
|
--this-border: transparent;
|
||||||
|
--this-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but.c-blue,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but.c-blue,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but.c-blue,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but.c-blue {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--modal-primary, #f04494);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相邻按钮分界线:legacy footer 自带 .but + .but 左分隔线,上方平铺规则的 border:0
|
||||||
|
会把它抹掉,这里按家族分隔色还原(单按钮无相邻兄弟,天然无分界线)。
|
||||||
|
需排在 .c-blue 规则之后:确认提交等主按钮同受 border:0 影响,后者同名优先级。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but + .but,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but + .but,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but + .but,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but + .but {
|
||||||
|
border-left: 1px solid var(--modal-line-soft, rgba(50, 50, 50, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停反馈(参考 Element 卡片 footer 的 hover 语言):整块区域浅灰底 + 文字转主色,
|
||||||
|
次级按钮 hover 升为主色、主按钮略加深;平铺按钮已满幅,底色即铺满整个区域,
|
||||||
|
基础 .but 的 0.15s 过渡自动生效,并压掉 compat 层的 hover 半透明。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but:hover,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but:hover,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but:hover,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: color-mix(in srgb, var(--modal-ink, #333333) 5%, transparent);
|
||||||
|
color: var(--modal-primary, #f04494);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but.c-blue:hover,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but.c-blue:hover,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but.c-blue:hover,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but.c-blue:hover {
|
||||||
|
color: color-mix(in srgb, var(--modal-primary, #f04494) 82%, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单按钮 footer 占满整行(全局网格是 1fr 1fr,单按钮会只占一半)。 */
|
||||||
|
.aksrt-after-sale-apply-modal .but-average.modal-buts > .but:only-child,
|
||||||
|
.aksrt-after-sale-detail-modal .but-average.modal-buts > .but:only-child,
|
||||||
|
.aksrt-after-sale-expired-modal .but-average.modal-buts > .but:only-child,
|
||||||
|
.aksrt-after-sale-withdraw-modal .but-average.modal-buts > .but:only-child { grid-column: 1 / -1; }
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,27 @@
|
|||||||
z-index: 100055;
|
z-index: 100055;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* public.css redeclares .muted-box after legacy utilities, overriding its
|
||||||
|
background, radius, and padding utilities. Restore the reference box contract. */
|
||||||
|
.modal[data-aksrt-vip-points-modal] .muted-box.padding-h10 {
|
||||||
|
background-color: var(--this-bg, var(--muted-border-color));
|
||||||
|
border-radius: var(--main-radius);
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal[data-aksrt-vip-points-modal] .muted-box.padding-10,
|
||||||
|
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||||
|
background-color: var(--this-bg, var(--muted-border-color));
|
||||||
|
border-radius: var(--main-radius);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: var(--muted-2-color, #999);
|
||||||
|
}
|
||||||
|
|
||||||
.pay-vip-form-row {
|
.pay-vip-form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -102,21 +123,85 @@
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v2 {
|
/* 会员弹窗骨架屏(pay-vip.js vipSkeleton()):仿真实弹窗布局——
|
||||||
|
用户框 + 等级卡行 + 双列套餐面板。线条/色块与 shimmer 动画复用全局
|
||||||
|
.aksrt-modal-runtime__skeleton-line/-block 与 @keyframes
|
||||||
|
aksrt-modal-skeleton(build/public.min.css 已全局入队);
|
||||||
|
这里只定义 VIP 专属形状(头像圆、tab 卡、双列 grid、按钮条),
|
||||||
|
与 .pay-vip-form-row 的 640px 双列阈值保持一致。 */
|
||||||
|
.aksrt-vip-modal-skeleton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 10px;
|
||||||
gap: 4px;
|
}
|
||||||
padding: 8px 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;
|
border-radius: 8px;
|
||||||
color: #fff;
|
background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16));
|
||||||
background: linear-gradient(90deg, #ffb347, #ff6b6b);
|
background-size: 200% 100%;
|
||||||
|
animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v2.level-2 {
|
@media (min-width: 640px) {
|
||||||
background: linear-gradient(90deg, #9b59b6, #3f51b5);
|
.aksrt-vip-modal-skeleton__pane {
|
||||||
}
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
}
|
||||||
.vip-expdate-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
+9
-14
@@ -5936,7 +5936,7 @@ body:has(.modal.in) .float-btn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.payvip-icon:before {
|
.payvip-icon:before {
|
||||||
content: '\f219';
|
content: '\f3a5';
|
||||||
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
||||||
font-size: 3.5em;
|
font-size: 3.5em;
|
||||||
right: -0.2em !important;
|
right: -0.2em !important;
|
||||||
@@ -5944,16 +5944,6 @@ body:has(.modal.in) .float-btn,
|
|||||||
opacity: 0.06;
|
opacity: 0.06;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v1 {
|
|
||||||
background: linear-gradient(60deg, #f0bf81 0, #fdefd6 30%, #f2deb9 40%, #fdc277 80%, #f9e1b7 100%);
|
|
||||||
color: #8c5728;
|
|
||||||
--this-color: #8c5728;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vipbg-v1:before {
|
|
||||||
content: '\f006';
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-texticon {
|
.vip-texticon {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -9665,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
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
.friend-card__avatar {
|
.friend-card__avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-avatar, 50%);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
|
|||||||
@@ -114,6 +114,10 @@
|
|||||||
border-radius: var(--main-radius, 8px);
|
border-radius: var(--main-radius, 8px);
|
||||||
background: var(--main-bg-color, #fff);
|
background: var(--main-bg-color, #fff);
|
||||||
}
|
}
|
||||||
|
/* 滚动发生在 dialog 本体(modal-runtime 的 overflow:auto),内容超高时右缘会露出
|
||||||
|
滚动栏;与售后/申诉弹窗口径一致:完全隐藏滚动条(含细滚动条),滚动行为不变。 */
|
||||||
|
.aksrt-order-detail-modal .aksrt-modal-runtime__dialog { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
.aksrt-order-detail-modal .aksrt-modal-runtime__dialog::-webkit-scrollbar { display: none; }
|
||||||
.aksrt-order-detail-modal .aksrt-modal-runtime__content { display: flex; flex-direction: column; gap: 18px; padding: 16px 18px 0; }
|
.aksrt-order-detail-modal .aksrt-modal-runtime__content { display: flex; flex-direction: column; gap: 18px; padding: 16px 18px 0; }
|
||||||
|
|
||||||
.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; }
|
.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; }
|
||||||
@@ -159,10 +163,22 @@
|
|||||||
.aksrt-order-detail__refresh-state p { margin: 0; }
|
.aksrt-order-detail__refresh-state p { margin: 0; }
|
||||||
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
|
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
|
||||||
|
|
||||||
/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts(禁封申诉弹窗同式),此处仅保留吸底定位与通栏背景。
|
/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts,此处仅保留吸底定位与通栏背景。
|
||||||
主题 modal-buts 自带 margin-top:16px,吸底场景下用更高优先级清掉,避免内容与吸底条之间出现空隙。 */
|
主题 modal-buts 自带 margin-top:16px,吸底场景下用更高优先级清掉,避免内容与吸底条之间出现空隙。
|
||||||
.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; margin: 0 -18px; padding: 13px 18px calc(13px + env(safe-area-inset-bottom)); border-top: 1px solid var(--detail-line); background: var(--detail-surface); }
|
footer 不留垂直内边距(水平同理归零)——上下 13px 并入按钮(min-height 64 = 13+38+13),
|
||||||
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts { margin-top: 0; }
|
按钮盒即整个底部区域;安全区留在 footer 底部,避免按钮文字沉入 Home 指示条。 */
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; margin: 0 -18px; padding: 0 0 env(safe-area-inset-bottom, 0); border-top: 1px solid var(--detail-line); background: var(--detail-surface); }
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts { margin-top: 0; gap: 0; }
|
||||||
|
/* 底部按钮观感与售后弹窗家族对齐(modal-runtime 同款平铺规则):去胶囊底色/边框/圆角,
|
||||||
|
文字色区分主次;仅「关闭」单按钮时占满整行,即售后详细「我知道了」的呈现。
|
||||||
|
分界线与悬停底色需排在 .c-blue 规则之后:主按钮同受 border:0 影响与主色覆盖,同名优先级看先后。 */
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but { min-height: 64px; border: 0; border-radius: 0; box-shadow: none; background: transparent; color: var(--modal-muted, #777); font-weight: 600; cursor: pointer; --this-bg: transparent; --this-border: transparent; --this-shadow: none; }
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but.c-blue { background: transparent; border: 0; color: var(--modal-primary, #f04494); }
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but + .but { border-left: 1px solid var(--detail-line); }
|
||||||
|
/* 悬停反馈与售后弹窗家族同款:整块区域浅灰底 + 文字转主色(次级升主色、主按钮略加深)。 */
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but:hover { opacity: 1; background: color-mix(in srgb, var(--modal-ink, #333333) 5%, transparent); color: var(--modal-primary, #f04494); }
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but.c-blue:hover { color: color-mix(in srgb, var(--modal-primary, #f04494) 82%, #000000); }
|
||||||
|
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts > .but:only-child { grid-column: 1 / -1; }
|
||||||
|
|
||||||
/* Orders extensions */
|
/* Orders extensions */
|
||||||
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||||
@@ -195,10 +211,5 @@ html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { backgro
|
|||||||
.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||||
.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
||||||
.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
|
.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
|
||||||
.aksrt-order-detail__after-sale-meta { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 10px; background: var(--detail-surface-3); }
|
|
||||||
.aksrt-order-detail__after-sale-meta > div { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--detail-line); font-size: 12.5px; }
|
|
||||||
.aksrt-order-detail__after-sale-meta > div:last-child { border-bottom: 0; }
|
|
||||||
.aksrt-order-detail__after-sale-meta dt { color: var(--detail-muted); }
|
|
||||||
.aksrt-order-detail__after-sale-meta dd { margin: 0; color: var(--detail-ink); font-size: 12.5px; text-align: right; word-break: break-word; }
|
|
||||||
.aksrt-order-detail__after-sale-field { color: var(--detail-muted); }
|
.aksrt-order-detail__after-sale-field { color: var(--detail-muted); }
|
||||||
.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; gap: 8px; }
|
.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||||
@@ -125,6 +125,8 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
|
/* 用户中心头图卡保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||||
|
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -88,6 +88,8 @@
|
|||||||
.aksrt-author-hero__avatar img {
|
.aksrt-author-hero__avatar img {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
|
/* 公开主页 hero 头像保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||||
|
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 4px solid var(--surface-1);
|
border: 4px solid var(--surface-1);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
|||||||
+14
-4
@@ -38,6 +38,14 @@
|
|||||||
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCutCount(value) {
|
||||||
|
var number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return String(value);
|
||||||
|
if (number > 999999) return String(Math.round(number / 10000)) + 'W+';
|
||||||
|
if (number > 9999) return String(Math.round(number / 1000) / 10) + 'W+';
|
||||||
|
return String(number);
|
||||||
|
}
|
||||||
|
|
||||||
function api(path, options) {
|
function api(path, options) {
|
||||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||||
return {
|
return {
|
||||||
@@ -64,16 +72,18 @@
|
|||||||
var walletNumber = Number(wallet);
|
var walletNumber = Number(wallet);
|
||||||
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
||||||
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
||||||
element.textContent = walletText;
|
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText;
|
||||||
element.title = walletText;
|
element.textContent = text;
|
||||||
|
element.title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (points !== undefined && points !== null && points !== '') {
|
if (points !== undefined && points !== null && points !== '') {
|
||||||
var pointsNumber = Number(points);
|
var pointsNumber = Number(points);
|
||||||
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
||||||
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
||||||
element.textContent = pointsText;
|
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(points) : pointsText;
|
||||||
element.title = pointsText;
|
element.textContent = text;
|
||||||
|
element.title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,6 @@
|
|||||||
this.bindFeatured();
|
this.bindFeatured();
|
||||||
this.bindPay();
|
this.bindPay();
|
||||||
this.bindLeaveWarning();
|
this.bindLeaveWarning();
|
||||||
this.bindAi();
|
|
||||||
this.bindTagActive();
|
this.bindTagActive();
|
||||||
this.loadFeaturedFromDom();
|
this.loadFeaturedFromDom();
|
||||||
},
|
},
|
||||||
@@ -635,67 +634,6 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
bindAi: function () {
|
|
||||||
var ai = cfg.ai || {};
|
|
||||||
if (!ai.available) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
qsa('.aksrt-ai-assist-btn').forEach(function (btn) {
|
|
||||||
btn.hidden = false;
|
|
||||||
btn.addEventListener('click', function () {
|
|
||||||
var action = btn.getAttribute('data-ai-action');
|
|
||||||
var content = getEditorContent();
|
|
||||||
if (!content || content.replace(/<[^>]+>/g, '').trim().length < 10) {
|
|
||||||
notyf(i18n.needContent || '请先填写正文内容', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var toastId = action === 'tags'
|
|
||||||
? 'aksrt-front-submit-ai-tags-toast'
|
|
||||||
: 'aksrt-front-submit-ai-summary-toast';
|
|
||||||
var old = btn.textContent;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = i18n.aiGenerating || '...';
|
|
||||||
notyf(i18n.aiGenerating || '正在生成…', 'load', '', toastId);
|
|
||||||
fetch((cfg.restUrl || '').replace(/\/$/, '') + '/ai/' + action, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-WP-Nonce': cfg.restNonce || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ content: content }),
|
|
||||||
})
|
|
||||||
.then(function (r) {
|
|
||||||
return r.json();
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
var text = (res && (res.data || res).text) || (res && res.result) || '';
|
|
||||||
if (!text) {
|
|
||||||
throw new Error(i18n.aiFailed || 'AI failed');
|
|
||||||
}
|
|
||||||
if (action === 'summary') {
|
|
||||||
var ex = qs('[name="excerpt"]');
|
|
||||||
if (ex) {
|
|
||||||
ex.value = text;
|
|
||||||
}
|
|
||||||
} else if (action === 'tags') {
|
|
||||||
var tags = qs('[name="tags"]');
|
|
||||||
if (tags) {
|
|
||||||
tags.value = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notyf(i18n.aiFilled || '已填入,可继续编辑', 'success', 5, toastId);
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
notyf(err.message || i18n.aiFailed || 'AI 生成失败', 'danger', 5, toastId);
|
|
||||||
})
|
|
||||||
.finally(function () {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = old;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
||||||
|
|||||||
+197
-43
@@ -4,9 +4,12 @@
|
|||||||
* 与「禁封/举报」链路同构(report.js / user-ban.js):
|
* 与「禁封/举报」链路同构(report.js / user-ban.js):
|
||||||
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
|
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
|
||||||
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
|
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
|
||||||
|
* - [data-aksrt-after-sale-withdraw] → 撤销售后二次确认弹窗(仅待审核工单),确认后 DELETE 工单
|
||||||
* 两个入口的 data-* 属性携带订单与售后状态载荷(同 report.js 的 data-aksrt-report),
|
* 两个入口的 data-* 属性携带订单与售后状态载荷(同 report.js 的 data-aksrt-report),
|
||||||
* 点击即同步弹窗,不做前置请求(避免首次点击无反馈)。
|
* 点击即同步弹窗,不做前置请求(避免首次点击无反馈)。
|
||||||
* - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale
|
* - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale
|
||||||
|
* - 撤销走 DELETE 同端点(仅待审核工单,后端置为已取消)
|
||||||
|
* - 提交与撤销前先过人机验证(after_sale_apply / after_sale_cancel,场景未启用自动放行)
|
||||||
* - 举证图片走 /orders/{order_number}/after-sale/upload-image(复用评论图片上传校验)
|
* - 举证图片走 /orders/{order_number}/after-sale/upload-image(复用评论图片上传校验)
|
||||||
*
|
*
|
||||||
* 按钮形态由订单视图模型的 after_sale 状态决定(UserOrders::after_sale_state):
|
* 按钮形态由订单视图模型的 after_sale 状态决定(UserOrders::after_sale_state):
|
||||||
@@ -88,11 +91,6 @@
|
|||||||
return '¥' + raw;
|
return '¥' + raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
function aftersaleAmountText(value, order) {
|
|
||||||
var amount = String(value == null ? '' : value);
|
|
||||||
return order && order.is_points ? amount + ' ' + String((order && order.currency_symbol) || '积分') : '¥' + amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function orderSummaryHtml(order) {
|
function orderSummaryHtml(order) {
|
||||||
if (!order) return '';
|
if (!order) return '';
|
||||||
return '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
|
return '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
|
||||||
@@ -166,6 +164,46 @@
|
|||||||
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
|
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理记录节点标题:用户侧提交/撤脱离不开工单状态,单独措辞;
|
||||||
|
// 后台提交直接用目标状态,与后台列表口径一致。
|
||||||
|
function handlingEntryTitle(entry) {
|
||||||
|
var status = String((entry && entry.status) || '');
|
||||||
|
if (String((entry && entry.source) || '') === 'user') {
|
||||||
|
return status === 'cancelled' ? '用户撤销售后' : '用户提交申请';
|
||||||
|
}
|
||||||
|
return STATUS_LABELS[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理记录时间线:左列绳结状态条(节点 + 状态 + 时间),右列处理备注。
|
||||||
|
// 数据源为 latest.handling_log(工单行内 JSON,按时间正序)。
|
||||||
|
function handlingLogHtml(log) {
|
||||||
|
var entries = Array.isArray(log) ? log : [];
|
||||||
|
if (!entries.length) {
|
||||||
|
return '<div class="aksrt-after-sale-log"><div class="aksrt-after-sale-log__empty">暂无处理记录</div></div>';
|
||||||
|
}
|
||||||
|
var html = '<div class="aksrt-after-sale-log"><div class="muted-2-color em09 mb6">处理记录</div>';
|
||||||
|
html += '<ol class="aksrt-after-sale-timeline">';
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
var status = String((entry && entry.status) || '');
|
||||||
|
var note = String((entry && entry.note) || '');
|
||||||
|
html += '<li class="aksrt-after-sale-timeline__item is-' + escapeHtml(status) + '">';
|
||||||
|
html += '<span class="aksrt-after-sale-timeline__knot"></span>';
|
||||||
|
html += '<div class="aksrt-after-sale-timeline__body">';
|
||||||
|
// [节点][处理状态][时间] 同一行,处理备注另起一行
|
||||||
|
html += '<div class="aksrt-after-sale-timeline__head">' +
|
||||||
|
'<span class="aksrt-after-sale-timeline__status">' + escapeHtml(handlingEntryTitle(entry)) + '</span>' +
|
||||||
|
'<time class="aksrt-after-sale-timeline__time">' + escapeHtml(String((entry && entry.created_at) || '')) + '</time>' +
|
||||||
|
'</div>';
|
||||||
|
if (note !== '') {
|
||||||
|
html += '<p class="aksrt-after-sale-timeline__note">' + escapeHtml(note).replace(/\r\n|\r|\n/g, '<br>') + '</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '</li>';
|
||||||
|
});
|
||||||
|
html += '</ol></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function buildDetailHtml(order) {
|
function buildDetailHtml(order) {
|
||||||
var afterSale = (order && order.after_sale) || {};
|
var afterSale = (order && order.after_sale) || {};
|
||||||
var latest = afterSale.latest || {};
|
var latest = afterSale.latest || {};
|
||||||
@@ -178,15 +216,19 @@
|
|||||||
|
|
||||||
html += '<div class="aksrt-after-sale-detail__statusRow">' +
|
html += '<div class="aksrt-after-sale-detail__statusRow">' +
|
||||||
'<span class="muted-2-color em09">售后状态</span>' +
|
'<span class="muted-2-color em09">售后状态</span>' +
|
||||||
|
'<span class="aksrt-after-sale-detail__statusCell">' +
|
||||||
'<span class="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
|
'<span class="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
|
||||||
'</div>';
|
// 仅「待审核」可撤销(后端 cancel 仅放行 pending):入口随状态徽章渲染。
|
||||||
|
(status === 'pending'
|
||||||
|
? '<button type="button" class="aksrt-after-sale-withdraw" data-aksrt-after-sale-withdraw="' + escapeHtml((order && order.order_number) || '') + '">撤销售后<i class="fa fas fa-angle-right" aria-hidden="true"></i></button>'
|
||||||
|
: '') +
|
||||||
|
'</span></div>';
|
||||||
|
|
||||||
html += '<div class="aksrt-after-sale-detail__rows">';
|
html += '<div class="aksrt-after-sale-detail__rows">';
|
||||||
html += detailRow('申请时间', String(latest.created_at || ''));
|
html += detailRow('申请时间', String(latest.created_at || ''));
|
||||||
html += detailRow('退款金额', aftersaleAmountText(latest.refund_amount, order));
|
|
||||||
html += detailRow('处理备注', String(latest.admin_note || ''));
|
|
||||||
html += detailRow('完成时间', String(latest.completed_at || ''));
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
// 处理记录:每次后台提交一条,替代原先单行的「处理备注 / 完成时间」。
|
||||||
|
html += handlingLogHtml(latest.handling_log);
|
||||||
|
|
||||||
// 申请表单分行展示:预置原因 / 申请资料 / 申请说明各自独立成行,
|
// 申请表单分行展示:预置原因 / 申请资料 / 申请说明各自独立成行,
|
||||||
// 与上方「申请时间」等明细行同格式;老数据无 apply_form 时兜底展示融合串。
|
// 与上方「申请时间」等明细行同格式;老数据无 apply_form 时兜底展示融合串。
|
||||||
@@ -238,6 +280,19 @@
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 撤销售后二次确认弹窗:待审核工单入口点入,确认后 DELETE 售后工单。
|
||||||
|
function buildWithdrawHtml(orderNumber) {
|
||||||
|
var html = '<div class="aksrt-after-sale-withdraw-modal">';
|
||||||
|
html += colorfulHeader('撤销售后', 'fa-undo-alt');
|
||||||
|
html += '<div class="aksrt-after-sale-withdraw__body em09">撤销后本次售后申请将关闭,订单恢复为可再次申请售后的状态。确定撤销吗?</div>';
|
||||||
|
html += '<div class="but-average modal-buts">' +
|
||||||
|
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
|
||||||
|
'<button type="button" class="but c-blue" data-aksrt-after-sale-withdraw-confirm="' + escapeHtml(orderNumber) + '">确认撤销</button>' +
|
||||||
|
'</div>';
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(options) {
|
function openModal(options) {
|
||||||
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
|
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
|
||||||
notyf('售后组件加载失败,请刷新页面后重试', 'danger');
|
notyf('售后组件加载失败,请刷新页面后重试', 'danger');
|
||||||
@@ -277,6 +332,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openWithdraw(orderNumber) {
|
||||||
|
if (!orderNumber) return;
|
||||||
|
return openModal({
|
||||||
|
html: buildWithdrawHtml(orderNumber),
|
||||||
|
className: 'aksrt-after-sale-withdraw-modal',
|
||||||
|
kind: 'after-sale-withdraw',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析按钮 data-* 上的售后载荷(与举报链路 data-aksrt-report 同模式):
|
* 解析按钮 data-* 上的售后载荷(与举报链路 data-aksrt-report 同模式):
|
||||||
* 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。
|
* 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。
|
||||||
@@ -407,6 +471,22 @@
|
|||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearToast(id) {
|
||||||
|
var toast = document.getElementById(id);
|
||||||
|
if (toast) toast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交前的人机验证:场景未启用时无挂载点,run 直接 resolve {};
|
||||||
|
// 用户中途取消验证时 run resolve null,此时还原按钮并中止提交。
|
||||||
|
function runCaptcha(scene) {
|
||||||
|
if (!window.AKSRTCaptcha || typeof window.AKSRTCaptcha.run !== 'function') {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
return window.AKSRTCaptcha.run(scene).then(function (payload) {
|
||||||
|
return payload === null ? Promise.reject({ cancelled: true }) : (payload || {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function submitApply(shell) {
|
function submitApply(shell) {
|
||||||
var form = shell.querySelector('[data-aksrt-after-sale-order]');
|
var form = shell.querySelector('[data-aksrt-after-sale-order]');
|
||||||
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
|
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
|
||||||
@@ -427,41 +507,97 @@
|
|||||||
if (submitBtn) submitBtn.disabled = true;
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
notyf('正在提交售后申请…', 'load', 3000, 'aksrt-after-sale-submit');
|
notyf('正在提交售后申请…', 'load', 3000, 'aksrt-after-sale-submit');
|
||||||
|
|
||||||
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
var restoreButton = function () {
|
||||||
method: 'POST',
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
headers: {
|
};
|
||||||
'Content-Type': 'application/json',
|
var sendApply = function (captchaPayload) {
|
||||||
'X-WP-Nonce': NONCE,
|
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||||||
},
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
headers: {
|
||||||
reason: composeReason(shell),
|
'Content-Type': 'application/json',
|
||||||
reason_label: selectedReasonLabel(shell),
|
'X-WP-Nonce': NONCE,
|
||||||
materials: collectMaterials(shell),
|
},
|
||||||
description: reasonText,
|
body: JSON.stringify(Object.assign({
|
||||||
images: collectImages(shell),
|
reason: composeReason(shell),
|
||||||
}),
|
reason_label: selectedReasonLabel(shell),
|
||||||
})
|
materials: collectMaterials(shell),
|
||||||
.then(function (res) { return res.json(); })
|
description: reasonText,
|
||||||
.then(function (result) {
|
images: collectImages(shell),
|
||||||
if (result && result.success) {
|
}, captchaPayload)),
|
||||||
window.AksrtModalRuntime.close('submit');
|
})
|
||||||
notyf('售后申请已提交', 'success');
|
.then(function (res) { return res.json(); })
|
||||||
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
.then(function (result) {
|
||||||
window.AKSRTOrderCenter.refresh(orderNumber);
|
if (result && result.success) {
|
||||||
|
window.AksrtModalRuntime.close('submit');
|
||||||
|
notyf('售后申请已提交', 'success');
|
||||||
|
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
||||||
|
window.AKSRTOrderCenter.refresh(orderNumber);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
restoreButton();
|
||||||
|
notyf((result && result.message) || '售后申请提交失败', 'danger');
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
if (submitBtn) submitBtn.disabled = false;
|
.catch(function () {
|
||||||
notyf((result && result.message) || '售后申请提交失败', 'danger');
|
restoreButton();
|
||||||
}
|
notyf('网络错误,请重试', 'danger');
|
||||||
|
})
|
||||||
|
.then(function () { clearToast('aksrt-after-sale-submit'); });
|
||||||
|
};
|
||||||
|
|
||||||
|
runCaptcha('after_sale_apply').then(sendApply).catch(function (error) {
|
||||||
|
restoreButton();
|
||||||
|
clearToast('aksrt-after-sale-submit');
|
||||||
|
if (error && error.cancelled) return;
|
||||||
|
notyf('人机验证失败,请重试', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销售后:待审核工单置为已取消(DELETE 售后工单),成功后刷新订单列表与统计。
|
||||||
|
function submitWithdraw(shell, button) {
|
||||||
|
var orderNumber = button.getAttribute('data-aksrt-after-sale-withdraw-confirm') || '';
|
||||||
|
if (!orderNumber || button.disabled) return;
|
||||||
|
button.disabled = true;
|
||||||
|
notyf('正在撤销售后申请…', 'load', 3000, 'aksrt-after-sale-withdraw');
|
||||||
|
|
||||||
|
var restoreButton = function () {
|
||||||
|
button.disabled = false;
|
||||||
|
};
|
||||||
|
var sendWithdraw = function (captchaPayload) {
|
||||||
|
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': NONCE,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(captchaPayload || {}),
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.then(function (res) { return res.json(); })
|
||||||
if (submitBtn) submitBtn.disabled = false;
|
.then(function (result) {
|
||||||
notyf('网络错误,请重试', 'danger');
|
if (result && result.success) {
|
||||||
})
|
window.AksrtModalRuntime.close('withdraw');
|
||||||
.then(function () {
|
notyf('售后申请已撤销', 'success');
|
||||||
var toast = document.getElementById('aksrt-after-sale-submit');
|
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
||||||
if (toast) toast.remove();
|
window.AKSRTOrderCenter.refresh(orderNumber);
|
||||||
});
|
}
|
||||||
|
} else {
|
||||||
|
restoreButton();
|
||||||
|
notyf((result && result.message) || '撤销失败,请重试', 'danger');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
restoreButton();
|
||||||
|
notyf('网络错误,请重试', 'danger');
|
||||||
|
})
|
||||||
|
.then(function () { clearToast('aksrt-after-sale-withdraw'); });
|
||||||
|
};
|
||||||
|
|
||||||
|
runCaptcha('after_sale_cancel').then(sendWithdraw).catch(function (error) {
|
||||||
|
restoreButton();
|
||||||
|
clearToast('aksrt-after-sale-withdraw');
|
||||||
|
if (error && error.cancelled) return;
|
||||||
|
notyf('人机验证失败,请重试', 'danger');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindShell(shell) {
|
function bindShell(shell) {
|
||||||
@@ -502,13 +638,31 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再次申请售后:载荷已随按钮渲染,直接开申请表单(时效/工单状态随列表刷新更新)。
|
// 再次申请售后:载荷已随按钮渲染。can_reapply 必然伴随 has_application=true,
|
||||||
|
// 直接走 openApplyFlow 会被 has_application 分流回「售后详细」打转;
|
||||||
|
// 清除标记落回申请表单分支(expired 超时效拦截仍然生效)。
|
||||||
var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]');
|
var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]');
|
||||||
if (reapplyBtn) {
|
if (reapplyBtn) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload'));
|
var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload'));
|
||||||
|
if (payload && payload.after_sale) payload.after_sale.has_application = false;
|
||||||
window.AksrtModalRuntime.close('replace');
|
window.AksrtModalRuntime.close('replace');
|
||||||
openApplyFlow(payload);
|
openApplyFlow(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销售后:入口在售后详细弹窗状态行(仅待审核),点击先弹二次确认。
|
||||||
|
var withdrawBtn = event.target.closest('[data-aksrt-after-sale-withdraw]');
|
||||||
|
if (withdrawBtn) {
|
||||||
|
event.preventDefault();
|
||||||
|
openWithdraw(withdrawBtn.getAttribute('data-aksrt-after-sale-withdraw') || '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var withdrawConfirm = event.target.closest('[data-aksrt-after-sale-withdraw-confirm]');
|
||||||
|
if (withdrawConfirm) {
|
||||||
|
event.preventDefault();
|
||||||
|
submitWithdraw(shell, withdrawConfirm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -106,26 +106,6 @@
|
|||||||
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
|
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatewayResultText(value) {
|
|
||||||
if (!value) return "";
|
|
||||||
var result = value;
|
|
||||||
if (typeof value === "string") {
|
|
||||||
try { result = JSON.parse(value); } catch (_) { return String(value); }
|
|
||||||
}
|
|
||||||
if (!result || typeof result !== "object") return String(result);
|
|
||||||
var labels = { succeeded: "退款成功", failed: "渠道明确拒绝", pending: "渠道处理中", unknown: "结果待确认" };
|
|
||||||
var parts = [];
|
|
||||||
if (result.status) parts.push(labels[String(result.status)] || String(result.status));
|
|
||||||
if (result.message) parts.push(String(result.message));
|
|
||||||
if (result.refund_id) parts.push("退款单号 " + String(result.refund_id));
|
|
||||||
return parts.join(" · ") || "渠道暂未提供可读结果";
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterSaleAmount(value, order) {
|
|
||||||
var amount = String(value == null ? "" : value);
|
|
||||||
return order && order.is_points ? amount + " " + String(order.currency_symbol || "积分") : "¥" + amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function colorfulHeader(title, icon) {
|
function colorfulHeader(title, icon) {
|
||||||
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
||||||
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
|
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
|
||||||
@@ -205,11 +185,6 @@
|
|||||||
} else if (item.reason) {
|
} else if (item.reason) {
|
||||||
html += '<p class="aksrt-order-detail__after-sale-reason">' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
|
html += '<p class="aksrt-order-detail__after-sale-reason">' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
|
||||||
}
|
}
|
||||||
html += '<dl class="aksrt-order-detail__after-sale-meta">';
|
|
||||||
html += "<div><dt>退款金额</dt><dd>" + escapeHtml(afterSaleAmount(item.refund_amount, order)) + "</dd></div>";
|
|
||||||
if (item.admin_note) html += "<div><dt>处理备注</dt><dd>" + escapeHtml(item.admin_note) + "</dd></div>";
|
|
||||||
if (item.gateway_result) html += "<div><dt>渠道结果</dt><dd>" + escapeHtml(gatewayResultText(item.gateway_result)) + "</dd></div>";
|
|
||||||
html += "</dl>";
|
|
||||||
// 查看详细:逐记录构造售后详细弹窗载荷(after_sale.latest 指向本条记录);
|
// 查看详细:逐记录构造售后详细弹窗载荷(after_sale.latest 指向本条记录);
|
||||||
// 再次申请仅对最新一条且订单级可再申请时开放(after_sales 按 id 倒序,首条即最新)。
|
// 再次申请仅对最新一条且订单级可再申请时开放(after_sales 按 id 倒序,首条即最新)。
|
||||||
var recordPayload = escapeHtml(JSON.stringify({
|
var recordPayload = escapeHtml(JSON.stringify({
|
||||||
@@ -230,14 +205,12 @@
|
|||||||
images: Array.isArray(item.images) ? item.images : [],
|
images: Array.isArray(item.images) ? item.images : [],
|
||||||
refund_amount: String(item.refund_amount || "0"),
|
refund_amount: String(item.refund_amount || "0"),
|
||||||
created_at: String(item.created_at || ""),
|
created_at: String(item.created_at || ""),
|
||||||
completed_at: String(item.completed_at || ""),
|
handling_log: Array.isArray(item.handling_log) ? item.handling_log : [],
|
||||||
admin_note: String(item.admin_note || ""),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
html += '<div class="aksrt-order-detail__after-sale-foot">';
|
html += '<div class="aksrt-order-detail__after-sale-foot">';
|
||||||
html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-detail="' + recordPayload + '">查看详细</button>';
|
html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-detail="' + recordPayload + '">查看详细</button>';
|
||||||
if (asStatus === "pending") html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-cancel="' + escapeHtml(order.order_number) + '">取消申请</button>';
|
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
html += "</li>";
|
html += "</li>";
|
||||||
});
|
});
|
||||||
@@ -252,9 +225,10 @@
|
|||||||
html += "</div>";
|
html += "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部按钮与禁封申诉弹窗同式:but-average modal-buts + .but / .but c-blue。
|
// 底部按钮与售后详细弹窗同式:but-average modal-buts;「关闭」与「我知道了」
|
||||||
|
// 同款(but c-blue),平铺观感由 orders.css 的 footer 按钮规则统一呈现。
|
||||||
html += '<footer class="aksrt-order-detail__actions but-average modal-buts">';
|
html += '<footer class="aksrt-order-detail__actions but-average modal-buts">';
|
||||||
html += '<button type="button" class="but" data-aksrt-modal-close>关闭</button>';
|
html += '<button type="button" class="but c-blue" data-aksrt-modal-close>关闭</button>';
|
||||||
if (allowedActions.can_cancel) html += '<button type="button" class="but" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
if (allowedActions.can_cancel) html += '<button type="button" class="but" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||||
if (allowedActions.can_pay) html += '<button type="button" class="but c-blue" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
|
if (allowedActions.can_pay) html += '<button type="button" class="but c-blue" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
|
||||||
// 售后入口:未申请时底部保留「申请售后」;已有申请时入口收拢到售后记录卡片的「查看详细」。
|
// 售后入口:未申请时底部保留「申请售后」;已有申请时入口收拢到售后记录卡片的「查看详细」。
|
||||||
@@ -662,25 +636,7 @@
|
|||||||
state.detailReplacementSession = null;
|
state.detailReplacementSession = null;
|
||||||
};
|
};
|
||||||
state.onDocumentClick = function (event) {
|
state.onDocumentClick = function (event) {
|
||||||
var afterSaleCancel = closest(event.target, "[data-aksrt-after-sale-cancel]");
|
// 取消/撤销售后已移入售后详细弹窗(order-after-sale.js 的「撤销售后」入口)。
|
||||||
if (afterSaleCancel && !event.__aksrtAfterSaleCancelHandled) {
|
|
||||||
var afterSaleNumber = afterSaleCancel.getAttribute("data-aksrt-after-sale-cancel") || "";
|
|
||||||
if (activeDetail(state, afterSaleNumber)) {
|
|
||||||
event.__aksrtAfterSaleCancelHandled = true;
|
|
||||||
event.preventDefault();
|
|
||||||
afterSaleCancel.disabled = true;
|
|
||||||
request(orderPath(afterSaleNumber, "/after-sale"), undefined, "DELETE", window.AksrtRequest.createIdempotencyKey()).then(function () {
|
|
||||||
notyf("售后申请已取消", "success");
|
|
||||||
invalidateOrder(afterSaleNumber);
|
|
||||||
openDetail(state, afterSaleNumber);
|
|
||||||
refreshView(state);
|
|
||||||
}).catch(function (error) {
|
|
||||||
afterSaleCancel.disabled = false;
|
|
||||||
notyf(errorMessage(error, "取消申请失败"), "danger");
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
|
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
|
||||||
var copy = closest(event.target, "[data-aksrt-order-copy]");
|
var copy = closest(event.target, "[data-aksrt-order-copy]");
|
||||||
if (copy) {
|
if (copy) {
|
||||||
|
|||||||
+110
-23
@@ -17,6 +17,14 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server-side schema sanitization limits these configured rich-text fields to post-safe HTML.
|
||||||
|
function htmlNode(tag, className, value) {
|
||||||
|
var el = document.createElement(tag);
|
||||||
|
if (className) el.className = className;
|
||||||
|
if (value !== undefined && value !== null) el.innerHTML = text(value);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function imageNode(src, className) {
|
function imageNode(src, className) {
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
var image = document.createElement("img");
|
var image = document.createElement("img");
|
||||||
@@ -61,7 +69,7 @@
|
|||||||
if (levelInfo && levelInfo.img_icon) {
|
if (levelInfo && levelInfo.img_icon) {
|
||||||
var img = node("img", "img-icon");
|
var img = node("img", "img-icon");
|
||||||
img.src = text(levelInfo.img_icon);
|
img.src = text(levelInfo.img_icon);
|
||||||
img.alt = text(levelInfo.name || "");
|
img.alt = text(levelInfo.name_text || levelInfo.name || "");
|
||||||
img.decoding = "async";
|
img.decoding = "async";
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
@@ -115,7 +123,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, htmlNode("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag));
|
||||||
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
|
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 +188,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");
|
||||||
@@ -384,12 +393,7 @@
|
|||||||
var avatarItem = node("li");
|
var avatarItem = node("li");
|
||||||
var avatarWrap = node("div", "avatar-img");
|
var avatarWrap = node("div", "avatar-img");
|
||||||
if (member.avatar_url) {
|
if (member.avatar_url) {
|
||||||
var avatar = node("img");
|
append(avatarWrap, imageNode(text(member.avatar_url), "avatar"));
|
||||||
avatar.src = text(member.avatar_url);
|
|
||||||
avatar.alt = "";
|
|
||||||
avatar.width = 42;
|
|
||||||
avatar.height = 42;
|
|
||||||
append(avatarWrap, avatar);
|
|
||||||
}
|
}
|
||||||
append(avatarItem, avatarWrap);
|
append(avatarItem, avatarWrap);
|
||||||
append(userBox, avatarItem);
|
append(userBox, avatarItem);
|
||||||
@@ -403,7 +407,7 @@
|
|||||||
} else if (member.state === "expired") {
|
} else if (member.state === "expired") {
|
||||||
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
||||||
} else if (catalog.desc) {
|
} else if (catalog.desc) {
|
||||||
append(userInfo, node("div", "muted-2-color em09", catalog.desc));
|
append(userInfo, htmlNode("div", "muted-2-color em09", catalog.desc));
|
||||||
}
|
}
|
||||||
append(userBox, userInfo);
|
append(userBox, userInfo);
|
||||||
append(modal, userBox);
|
append(modal, userBox);
|
||||||
@@ -439,8 +443,8 @@
|
|||||||
append(miniIcon, vipIcon(levelInfo));
|
append(miniIcon, vipIcon(levelInfo));
|
||||||
append(miniIconbox, miniIcon);
|
append(miniIconbox, miniIcon);
|
||||||
var miniInfo = node("div", "v-info");
|
var miniInfo = node("div", "v-info");
|
||||||
append(miniInfo, node("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
append(miniInfo, htmlNode("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
||||||
append(miniInfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
append(miniInfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||||
append(miniIconbox, miniInfo);
|
append(miniIconbox, miniInfo);
|
||||||
append(miniCard, miniIconbox);
|
append(miniCard, miniIconbox);
|
||||||
append(tab, miniCard);
|
append(tab, miniCard);
|
||||||
@@ -466,15 +470,12 @@
|
|||||||
append(vicon, vipIcon(levelInfo));
|
append(vicon, vipIcon(levelInfo));
|
||||||
append(iconbox, vicon);
|
append(iconbox, vicon);
|
||||||
var vinfo = node("div", "v-info");
|
var vinfo = node("div", "v-info");
|
||||||
append(vinfo, node("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
append(vinfo, htmlNode("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
||||||
append(vinfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
append(vinfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||||
append(iconbox, vinfo);
|
append(iconbox, vinfo);
|
||||||
append(info, iconbox);
|
append(info, iconbox);
|
||||||
if (levelInfo.equity) {
|
if (levelInfo.equity) {
|
||||||
var equity = document.createElement("ul");
|
append(info, htmlNode("div", "", levelInfo.equity));
|
||||||
equity.className = "v-equity";
|
|
||||||
equity.innerHTML = text(levelInfo.equity);
|
|
||||||
append(info, equity);
|
|
||||||
}
|
}
|
||||||
var infoBa = node("div", "abs-center vip-baicon");
|
var infoBa = node("div", "abs-center vip-baicon");
|
||||||
append(infoBa, vipIcon(levelInfo));
|
append(infoBa, vipIcon(levelInfo));
|
||||||
@@ -497,7 +498,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;
|
||||||
@@ -524,8 +525,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);
|
||||||
@@ -703,6 +702,46 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 骨架屏:仿弹窗真实布局(用户框 + 等级卡 + 双列套餐面板)。
|
||||||
|
// 线条/色块复用全局骨架类(build/public.min.css),VIP 专属形状见 vip-modal.css。
|
||||||
|
function vipSkeleton() {
|
||||||
|
var skeleton = node("div", "aksrt-vip-modal-skeleton");
|
||||||
|
skeleton.setAttribute("aria-live", "polite");
|
||||||
|
skeleton.setAttribute("aria-busy", "true");
|
||||||
|
|
||||||
|
// 用户框:头像圆 + 两行文字。
|
||||||
|
var user = node("div", "aksrt-vip-modal-skeleton__user");
|
||||||
|
append(user, node("span", "aksrt-vip-modal-skeleton__avatar"));
|
||||||
|
var userLines = node("span", "aksrt-vip-modal-skeleton__lines");
|
||||||
|
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||||
|
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||||
|
append(user, userLines);
|
||||||
|
append(skeleton, user);
|
||||||
|
|
||||||
|
// 等级卡行(vip-cardminis):两张 tab 占位。
|
||||||
|
var tabs = node("div", "aksrt-vip-modal-skeleton__tabs");
|
||||||
|
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||||
|
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||||
|
append(skeleton, tabs);
|
||||||
|
|
||||||
|
// 双列面板:左权益信息 + 右套餐区(套餐卡、支付方式行、提交按钮条)。
|
||||||
|
var pane = node("div", "aksrt-vip-modal-skeleton__pane");
|
||||||
|
var info = node("div", "aksrt-vip-modal-skeleton__info");
|
||||||
|
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||||
|
append(info, node("span", "aksrt-modal-runtime__skeleton-line"));
|
||||||
|
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||||
|
append(pane, info);
|
||||||
|
var products = node("div", "aksrt-vip-modal-skeleton__products");
|
||||||
|
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||||
|
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||||
|
append(products, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||||
|
append(products, node("span", "aksrt-vip-modal-skeleton__button"));
|
||||||
|
append(pane, products);
|
||||||
|
append(skeleton, pane);
|
||||||
|
|
||||||
|
return skeleton.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function vipPay(trigger) {
|
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
|
||||||
@@ -711,11 +750,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);
|
||||||
@@ -733,6 +772,53 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prefetchedLevels = Object.create(null);
|
||||||
|
|
||||||
|
// 与 openRemote 同一 cacheKey(method + AksrtRequest.url),命中同一份缓存/flight。
|
||||||
|
function vipModalCacheKey(level) {
|
||||||
|
return "GET:" + window.AksrtRequest.url("/pay/vip/modal", { vip_level: level });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 意图预热:悬停/聚焦开通入口时按等级预热弹窗数据(不打开弹窗,失败静默)。
|
||||||
|
// 点击打开时若预热请求在途,cache.acquire 的 flight 去重会自动合并。
|
||||||
|
function prefetchVipModal(trigger) {
|
||||||
|
if (!trigger || !window.AksrtModalCache || !window.AksrtRequest) return;
|
||||||
|
var level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
|
||||||
|
if (level < 1 || level > 2) level = 1;
|
||||||
|
if (prefetchedLevels[level]) return;
|
||||||
|
prefetchedLevels[level] = true;
|
||||||
|
var cache = window.AksrtModalCache;
|
||||||
|
if (cache.get && cache.get(vipModalCacheKey(level)).hit) return;
|
||||||
|
try {
|
||||||
|
cache.acquire(vipModalCacheKey(level), function () {
|
||||||
|
return window.AksrtRequest.request("/pay/vip/modal", {
|
||||||
|
method: "GET",
|
||||||
|
query: { vip_level: level },
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
ttl: 60000,
|
||||||
|
tags: ["vip"],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 预热失败不影响点击后的正常打开路径。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function vipTriggerFrom(event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.closest) return null;
|
||||||
|
return target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPrefetch() {
|
||||||
|
// pointerenter 不冒泡但走捕获阶段;focusin 冒泡,两者互补覆盖鼠标与键盘。
|
||||||
|
["pointerenter", "focusin"].forEach(function (type) {
|
||||||
|
document.addEventListener(type, function (event) {
|
||||||
|
prefetchVipModal(vipTriggerFrom(event));
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onDocClick(event) {
|
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;
|
||||||
@@ -773,6 +859,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>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
* Check-in CTA for header dropdown and compact card placements.
|
* Check-in CTA for header dropdown and compact card placements.
|
||||||
*
|
*
|
||||||
* @param int $user_id Current user ID.
|
* @param int $user_id Current user ID.
|
||||||
* @param bool $compact Render a short card-friendly label.
|
|
||||||
* @param string|null $class Override the classes and render the reference-parity
|
* @param string|null $class Override the classes and render the reference-parity
|
||||||
* <a> badge (user-card 签到书签); null keeps the header
|
* <a> badge (user-card 签到书签); null keeps the header
|
||||||
* dropdown <button> runtime.
|
* dropdown <a> runtime.
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_checkin_btn(int $user_id = 0, bool $compact = false, ?string $class = null): string {
|
function aksrt_header_user_checkin_btn(int $user_id = 0, ?string $class = null): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
||||||
return '';
|
return '';
|
||||||
@@ -56,38 +55,39 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
|
|
||||||
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
||||||
if ($signed) {
|
if ($signed) {
|
||||||
$icon = '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>';
|
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
// 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
$class . ' is-done',
|
$class,
|
||||||
$icon . '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
|
. esc_html__('已签到', 'aksrt')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
'but block c-yellow aksrt-header-checkin is-done',
|
'but block c-yellow is-done',
|
||||||
'<i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt'))
|
'<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||||
|
. esc_html__('今日已签到', 'aksrt')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$classes = $class ?? ('but block c-yellow aksrt-header-checkin' . ($compact ? ' aksrt-header-checkin--compact' : ''));
|
|
||||||
|
|
||||||
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
||||||
// reference parity renders an <a> so the badge inherits the card font
|
// reference parity renders an <a> so the badge inherits the card font
|
||||||
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
return '<a class="' . esc_attr($classes) . '" href="javascript:;" data-signin-btn'
|
return '<a class="' . esc_attr($class) . '" href="javascript:;" data-signin-btn'
|
||||||
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||||
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
||||||
. '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
. '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
||||||
. '</a>';
|
. '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<button type="button" class="' . esc_attr($classes) . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
return '<a class="but block c-yellow" href="javascript:;" data-signin-btn'
|
||||||
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
|
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||||
. '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>'
|
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
|
||||||
. '</button>';
|
. '<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||||
|
. '<span data-signin-label>' . esc_html__('签到领取今日奖励', 'aksrt') . '</span>'
|
||||||
|
. '</a>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -118,41 +118,65 @@ add_action('wp_footer', 'aksrt_render_signin_captcha');
|
|||||||
|
|
||||||
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
||||||
/**
|
/**
|
||||||
* Mini wallet strip: credits + cash balance.
|
* Mini wallet tiles: cash balance + credits (reference parity).
|
||||||
|
*
|
||||||
|
* Each tile is gated by its payment switch; an all-off configuration
|
||||||
|
* renders nothing at all.
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0) {
|
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$credits = aksrt_user_points($user_id);
|
|
||||||
$currency = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_currency_name()
|
|
||||||
: __('积分', 'aksrt');
|
|
||||||
$wallet = aksrt_user_balance_decimal($user_id);
|
|
||||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
|
||||||
|
|
||||||
$html = '<a class="aksrt-header-wallet flex ac jsb" href="' . esc_url($center) . '">';
|
$balance_on = !empty(\AKSRT\Settings\Settings::get_payment_recharge_settings()['enabled']);
|
||||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-database" aria-hidden="true"></i> '
|
$points_on = \AKSRT\Settings\Settings::credits_enabled();
|
||||||
. esc_html($currency) . ' <b data-aksrt-points-value data-credits-value>' . esc_html(number_format_i18n($credits)) . '</b></span>';
|
if (!$balance_on && !$points_on) {
|
||||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-money-bill" aria-hidden="true"></i> '
|
return '';
|
||||||
. esc_html__('余额', 'aksrt') . ' <b data-aksrt-wallet-value>' . esc_html(number_format_i18n($wallet, 2)) . '</b></span>';
|
}
|
||||||
$html .= '</a>';
|
|
||||||
return $html;
|
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||||
|
$box = '';
|
||||||
|
$balance_icon = aksrt_icon_markup('zibsvg-money-color-2', 'em14');
|
||||||
|
$points_icon = aksrt_icon_markup('zibsvg-points-color', 'em14');
|
||||||
|
|
||||||
|
if ($balance_on) {
|
||||||
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
|
. '<div class="muted-2-color px12">' . esc_html__('余额', 'aksrt')
|
||||||
|
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||||
|
. '<div class="flex jsb"><span class="font-bold c-blue-2" data-aksrt-wallet-value data-aksrt-count-format="cut-count">'
|
||||||
|
. esc_html(aksrt_cut_count((float) aksrt_user_balance_decimal($user_id)))
|
||||||
|
. '</span>' . $balance_icon . '</div></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($points_on) {
|
||||||
|
$currency = \AKSRT\Settings\Settings::get_currency_name();
|
||||||
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
|
. '<div class="muted-2-color px12">' . esc_html($currency)
|
||||||
|
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||||
|
. '<div class="flex jsb"><span class="font-bold c-yellow" data-aksrt-points-value data-aksrt-count-format="cut-count">'
|
||||||
|
. esc_html(aksrt_cut_count(aksrt_user_points($user_id)))
|
||||||
|
. '</span>' . $points_icon . '</div></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_header_user_badges_row')) :
|
if (!function_exists('aksrt_header_user_badges_row')) :
|
||||||
/**
|
/**
|
||||||
* Identity / medal chips under profile.
|
* Interaction stats row under the wallet (reference parity).
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0 || !function_exists('aksrt_render_user_identity_chips')) {
|
if ($user_id <= 0 || !function_exists('aksrt_get_user_badges')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// Certification is displayed beside the name and level icon in the profile row.
|
$items = aksrt_get_user_badges($user_id);
|
||||||
return '';
|
if ($items === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '<div class="em09 author-tag mb10 mt6 flex jc">' . $items . '</div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -173,30 +197,33 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_all()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
||||||
if ($level > 0) {
|
if ($level > 0) {
|
||||||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||||||
$name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
$name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||||
|
// 到期徽章语义对齐参考主题 zib_get_user_vip_exp_date_text():只有真的拿到
|
||||||
|
// 到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;显式永久
|
||||||
|
// 会员显示「永久会员」;拿不到到期数据时参考主题返回 false,整枚徽章不渲染。
|
||||||
$exp_label = '';
|
$exp_label = '';
|
||||||
if (function_exists('aksrt_get_vip_info')) {
|
if (function_exists('aksrt_get_user_vip_state')) {
|
||||||
$info = aksrt_get_vip_info($user_id);
|
$state = aksrt_get_user_vip_state($user_id);
|
||||||
$expires = max(0, (int) ($info['expires'] ?? 0));
|
$expires = max(0, (int) ($state['expires'] ?? 0));
|
||||||
$exp_label = $expires > 0
|
if ($expires > 0) {
|
||||||
? sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires))
|
$exp_label = $expires <= time()
|
||||||
: (string) ($info['expires_label'] ?? '');
|
? __('会员已过期', 'aksrt')
|
||||||
|
: wp_date('Y年m月d日', $expires);
|
||||||
|
} elseif (!empty($state['is_permanent'])) {
|
||||||
|
$exp_label = __('永久会员', 'aksrt');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$html = '<div class="mt10 em09 aksrt-header-payvip is-vip">';
|
$html = '<div class="mt10 em09" style="padding:2px;">';
|
||||||
$html .= '<span class="radius4 payvip-icon btn-block text-center vipbg-v2 level-' . (int) $level . ' aksrt-header-payvip__active">';
|
$html .= '<span class="radius4 payvip-icon btn-block text-center">';
|
||||||
$html .= '<span class="aksrt-header-payvip__level">';
|
$html .= function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id, 'em12 mr6', false) : '';
|
||||||
$html .= '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
$html .= '<span>' . wp_kses_post($name) . '</span>';
|
||||||
$html .= '<span>' . esc_html($name) . '</span>';
|
|
||||||
$html .= '</span>';
|
|
||||||
if ($exp_label !== '') {
|
if ($exp_label !== '') {
|
||||||
$html .= '<span class="aksrt-header-payvip__exp">' . esc_html($exp_label) . '</span>';
|
$html .= '<span class="ml10 badg jb-yellow vip-expdate-tag">'
|
||||||
|
. '<i class="fa fas fa-clock mr3" aria-hidden="true"></i>'
|
||||||
|
. esc_html($exp_label) . '</span>';
|
||||||
}
|
}
|
||||||
$html .= '</span></div>';
|
$html .= '</span></div>';
|
||||||
return $html;
|
return $html;
|
||||||
@@ -205,11 +232,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match check-in CTA geometry (full width, 6px radius) — not top-nav navbar-cta chips.
|
// 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
|
||||||
return '<div class="mt10 em09 aksrt-header-payvip">'
|
return '<div class="mt10 em09" style="padding:2px;">'
|
||||||
. '<a class="pay-vip but block payvip-icon aksrt-header-payvip__cta" href="javascript:;" vip-level="1">'
|
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
|
||||||
. '<i class="fa fas fa-gem" aria-hidden="true"></i>'
|
. '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>'
|
. esc_html__('开通会员 尊享会员权益', 'aksrt')
|
||||||
. '</a></div>';
|
. '</a></div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|||||||
@@ -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 '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,14 +351,14 @@ if (!function_exists('aksrt_get_user_badges')) {
|
|||||||
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
||||||
|
|
||||||
$html = '';
|
$html = '';
|
||||||
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('post') . $post_n . '</a>';
|
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-file-alt" aria-hidden="true"></i>' . $post_n . '</a>';
|
||||||
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('comment') . $com_n . '</a>';
|
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-comment-dots" aria-hidden="true"></i>' . $com_n . '</a>';
|
||||||
if ($followed_n) {
|
if ($followed_n) {
|
||||||
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
||||||
} elseif ($like_n) {
|
} elseif ($like_n) {
|
||||||
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '">' . aksrt_renderer_svg_icon('like') . $like_n . '</span>';
|
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '"><i class="fa fas fa-thumbs-up" aria-hidden="true"></i>' . $like_n . '</span>';
|
||||||
}
|
}
|
||||||
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '">' . aksrt_renderer_svg_icon('hot') . $view_n . '</span>';
|
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '"><i class="fa fas fa-fire" aria-hidden="true"></i>' . $view_n . '</span>';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter the author-card count badges row.
|
* Filter the author-card count badges row.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1309,6 +1309,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level));
|
$level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level));
|
||||||
|
$level_name_html = (string) ($level_settings['name_html'] ?? $level_name);
|
||||||
$price = $vip_minor === 0
|
$price = $vip_minor === 0
|
||||||
? esc_html__('免费', 'aksrt')
|
? esc_html__('免费', 'aksrt')
|
||||||
: $mark . esc_html(aksrt_format_price($value));
|
: $mark . esc_html(aksrt_format_price($value));
|
||||||
@@ -1319,7 +1320,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
|||||||
? ' data-toggle="tooltip" title="' . esc_attr($title) . '"'
|
? ' data-toggle="tooltip" title="' . esc_attr($title) . '"'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . esc_html($level_name) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . wp_kses_post($level_name_html) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = implode('', $items);
|
$html = implode('', $items);
|
||||||
@@ -1384,10 +1385,13 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
|||||||
$vip_action = $vip_level > 0
|
$vip_action = $vip_level > 0
|
||||||
? sprintf(__('升级%s', 'aksrt'), $level_name)
|
? sprintf(__('升级%s', 'aksrt'), $level_name)
|
||||||
: __('开通会员', 'aksrt');
|
: __('开通会员', 'aksrt');
|
||||||
|
$vip_action_html = $vip_level > 0
|
||||||
|
? __('升级', 'aksrt') . wp_kses_post((string) ($level_settings['name_html'] ?? $level_name))
|
||||||
|
: esc_html__('开通会员', 'aksrt');
|
||||||
$action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader';
|
$action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader';
|
||||||
$icon = aksrt_get_pay_vip_icon_html($level_settings);
|
$icon = aksrt_get_pay_vip_icon_html($level_settings);
|
||||||
$button = '<div class="badg c-yellow em09 mb6"><i class="fa fas fa-fw fa-info-circle mr6" aria-hidden="true"></i>' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '</div>';
|
$button = '<div class="badg c-yellow em09 mb6"><i class="fa fas fa-fw fa-info-circle mr6" aria-hidden="true"></i>' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '</div>';
|
||||||
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . esc_html($vip_action) . '</a>';
|
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . $vip_action_html . '</a>';
|
||||||
} elseif (!is_user_logged_in()) {
|
} elseif (!is_user_logged_in()) {
|
||||||
if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) {
|
if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) {
|
||||||
$button = '<button type="button" class="but jb-red padding-lg cashier-link' . $block_class . '" data-aksrt-pay-content="' . esc_attr((string) $post->ID) . '"><i class="fa fas fa-shopping-cart" aria-hidden="true"></i> ' . esc_html__('立即购买', 'aksrt') . '</button>';
|
$button = '<button type="button" class="but jb-red padding-lg cashier-link' . $block_class . '" data-aksrt-pay-content="' . esc_attr((string) $post->ID) . '"><i class="fa fas fa-shopping-cart" aria-hidden="true"></i> ' . esc_html__('立即购买', 'aksrt') . '</button>';
|
||||||
|
|||||||
@@ -285,11 +285,12 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
|||||||
}
|
}
|
||||||
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
||||||
|
|
||||||
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg');
|
// 会员/禁封徽章不再挂头像角标,改由名字行的会员位出图(aksrt_get_user_name)。
|
||||||
|
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg', true, false);
|
||||||
$name_html = aksrt_get_user_name([
|
$name_html = aksrt_get_user_name([
|
||||||
'id' => $author_id,
|
'id' => $author_id,
|
||||||
'class' => 'flex1 flex ac',
|
'class' => 'flex1 flex ac',
|
||||||
'vip' => false,
|
'vip' => true,
|
||||||
'medal' => true,
|
'medal' => true,
|
||||||
'auth' => true,
|
'auth' => true,
|
||||||
'level' => true,
|
'level' => true,
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ endif;
|
|||||||
* or Chinese keyword heuristics — that mangles intentional HTML.
|
* or Chinese keyword heuristics — that mangles intentional HTML.
|
||||||
*
|
*
|
||||||
* Rules:
|
* Rules:
|
||||||
* - Sanitize with a small allowlist.
|
* - Sanitize with WordPress post-safe HTML.
|
||||||
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
||||||
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
||||||
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
||||||
@@ -497,21 +497,7 @@ if (!function_exists('aksrt_format_vip_equity_html')) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowed = [
|
$clean = trim(wp_kses_post($raw));
|
||||||
'li' => ['class' => true],
|
|
||||||
'ul' => ['class' => true],
|
|
||||||
'ol' => ['class' => true],
|
|
||||||
'strong' => [],
|
|
||||||
'b' => [],
|
|
||||||
'em' => [],
|
|
||||||
'i' => ['class' => true],
|
|
||||||
'span' => ['class' => true],
|
|
||||||
'br' => [],
|
|
||||||
'p' => ['class' => true],
|
|
||||||
'div' => ['class' => true],
|
|
||||||
'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true],
|
|
||||||
];
|
|
||||||
$clean = trim(wp_kses($raw, $allowed));
|
|
||||||
if ($clean === '') {
|
if ($clean === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,10 +138,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$con = '';
|
$con = '';
|
||||||
|
|
||||||
if ($user_id > 0) {
|
if ($user_id > 0) {
|
||||||
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
||||||
$display_name = $user->display_name ?: $user->user_login;
|
$display_name = $user->display_name ?: $user->user_login;
|
||||||
$avatar = aksrt_get_avatar_html($user_id, 44, ['alt' => $display_name]);
|
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img', true, false);
|
||||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||||
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
||||||
// Level and certification icons ride inline to the right of the display name.
|
// Level and certification icons ride inline to the right of the display name.
|
||||||
$level_badge = (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html'))
|
$level_badge = (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html'))
|
||||||
@@ -174,13 +174,13 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
|
|
||||||
$msg_icon = '';
|
$msg_icon = '';
|
||||||
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
||||||
$msg_icon = '<a class="abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
$msg_icon = '<a class="msg-news-icon abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
||||||
$msg_icon .= '<i class="fa fas fa-bell" aria-hidden="true"></i>';
|
$msg_icon .= '<span class="toggle-radius msg-icon"><i class="fa fas fa-bell" aria-hidden="true"></i></span>';
|
||||||
if ($unread > 0) {
|
if ($unread > 0) {
|
||||||
$msg_icon .= '<span class="aksrt-notification-badge">' . esc_html((string) min(99, $unread)) . '</span>';
|
$msg_icon .= '<badge class="top">' . esc_html((string) min(99, $unread)) . '</badge>';
|
||||||
}
|
|
||||||
$msg_icon .= '</a>';
|
|
||||||
}
|
}
|
||||||
|
$msg_icon .= '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
$href = '<div class="flex jsa header-user-href">';
|
$href = '<div class="flex jsa header-user-href">';
|
||||||
$href .= '<a rel="nofollow" href="' . esc_url(function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')) . '">';
|
$href .= '<a rel="nofollow" href="' . esc_url(function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')) . '">';
|
||||||
@@ -224,6 +224,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
||||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
||||||
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
||||||
|
// legacy: 商城中心 → 支付中心(AKSRT 支付中心后台菜单)。
|
||||||
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('admin.php?page=aksrt_pay')) . '">';
|
||||||
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-credit-card" aria-hidden="true"></i></div>';
|
||||||
|
$href .= '<div class="c-yellow">' . esc_html__('支付中心', 'aksrt') . '</div></a>';
|
||||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
||||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
||||||
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
||||||
@@ -234,7 +238,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$con .= $avatar;
|
$con .= $avatar;
|
||||||
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
||||||
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
||||||
. '<span class="display-name text-ellipsis">' . esc_html($display_name) . '</span>' . $vip_badge . $medal_badge . $certification_badge . $level_badge
|
. '<name class="flex ac flex1">'
|
||||||
|
. '<a class="display-name text-ellipsis" href="' . esc_url(function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id)) . '">' . esc_html($display_name) . '</a>'
|
||||||
|
. $vip_badge . $medal_badge . $certification_badge . $level_badge
|
||||||
|
. '</name>'
|
||||||
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
||||||
$con .= '</div>';
|
$con .= '</div>';
|
||||||
$con .= $msg_icon;
|
$con .= $msg_icon;
|
||||||
@@ -264,17 +271,22 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
||||||
. esc_html__('注册', 'aksrt') . '</div></a>';
|
. esc_html__('注册', 'aksrt') . '</div></a>';
|
||||||
}
|
}
|
||||||
$href .= '<a rel="nofollow" href="' . esc_url(wp_lostpassword_url(home_url('/'))) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
// legacy: 找回密码 → 统一登录页 resetpassword 标签(带回跳)。
|
||||||
|
$reset_url = function_exists('aksrt_get_sign_page_url')
|
||||||
|
? aksrt_get_sign_page_url('resetpassword', [
|
||||||
|
'redirect_to' => remove_query_arg(
|
||||||
|
'redirect_to',
|
||||||
|
home_url(isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '/')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: wp_lostpassword_url(home_url('/'));
|
||||||
|
$href .= '<a target="_blank" rel="nofollow" href="' . esc_url($reset_url) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
||||||
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
||||||
$href .= '</div>';
|
$href .= '</div>';
|
||||||
|
|
||||||
$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>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,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 {
|
||||||
@@ -355,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,4 +150,13 @@ $status_options = [
|
|||||||
<div class="aksrt-orders-page__empty" data-aksrt-order-list><i class="fa fas fa-file-alt" aria-hidden="true"></i><h3><?php esc_html_e('还没有购买记录', 'aksrt'); ?></h3><p><?php esc_html_e('购买内容或开通权益后,订单会出现在这里。', 'aksrt'); ?></p></div>
|
<div class="aksrt-orders-page__empty" data-aksrt-order-list><i class="fa fas fa-file-alt" aria-hidden="true"></i><h3><?php esc_html_e('还没有购买记录', 'aksrt'); ?></h3><p><?php esc_html_e('购买内容或开通权益后,订单会出现在这里。', 'aksrt'); ?></p></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 售后人机验证挂载点(申请 / 撤销):场景未启用时 aksrt_render_captcha 无输出,
|
||||||
|
// 前端 order-after-sale.js 以挂载点存在与否决定是否先过验证。
|
||||||
|
if (function_exists('aksrt_render_captcha')) {
|
||||||
|
aksrt_render_captcha('after_sale_apply');
|
||||||
|
aksrt_render_captcha('after_sale_cancel');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if ($status === '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($vip_level > 0) {
|
if ($vip_level > 0) {
|
||||||
$title = (string) ($vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
$title = (string) ($vip_levels[$vip_level]['name_html'] ?? $vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||||
if ($expires <= 0 && ($vip_state['source'] ?? 'none') !== 'order') {
|
if ($expires <= 0 && ($vip_state['source'] ?? 'none') !== 'order') {
|
||||||
$status = __('永久会员', 'aksrt');
|
$status = __('永久会员', 'aksrt');
|
||||||
} else {
|
} else {
|
||||||
@@ -41,7 +41,7 @@ if ($vip_level > 0) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} elseif ($expired_level > 0) {
|
} elseif ($expired_level > 0) {
|
||||||
$title = (string) ($vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
$title = (string) ($vip_levels[$expired_level]['name_html'] ?? $vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||||
$status = __('已过期', 'aksrt');
|
$status = __('已过期', 'aksrt');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,13 +49,14 @@ $sidebar_level = $vip_level > 0 ? $vip_level : $expired_level;
|
|||||||
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
$sidebar_icon_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>
|
||||||
<span class="uc-sidebar-vip__content">
|
<span class="uc-sidebar-vip__content">
|
||||||
<strong><?php echo esc_html($title); ?></strong>
|
<strong><?php echo wp_kses_post($title); ?></strong>
|
||||||
<small><?php echo esc_html($status); ?></small>
|
<small><?php echo wp_kses_post($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,13 +38,15 @@ $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';
|
||||||
|
|
||||||
if ($level > 0) {
|
if ($level > 0) {
|
||||||
$vip_level_number = $level;
|
$vip_level_number = $level;
|
||||||
$vip_level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
$vip_level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||||
$level_desc = (string) ($levels[$level]['desc'] ?? '');
|
$level_desc = (string) ($levels[$level]['desc'] ?? '');
|
||||||
$vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt');
|
$vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt');
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ if ($level > 0) {
|
|||||||
$vip_state_label = __('生效中', 'aksrt');
|
$vip_state_label = __('生效中', 'aksrt');
|
||||||
$vip_expiry_text = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires));
|
$vip_expiry_text = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires));
|
||||||
if ($base_level > 0 && $base_level < $level) {
|
if ($base_level > 0 && $base_level < $level) {
|
||||||
$base_name = (string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level));
|
$base_name = wp_strip_all_tags((string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level)));
|
||||||
$base_expiry_text = $base_expires > 0
|
$base_expiry_text = $base_expires > 0
|
||||||
? wp_date('Y年m月d日', $base_expires)
|
? wp_date('Y年m月d日', $base_expires)
|
||||||
: __('永久', 'aksrt');
|
: __('永久', 'aksrt');
|
||||||
@@ -71,7 +75,7 @@ if ($level > 0) {
|
|||||||
$vip_state = 'expired';
|
$vip_state = 'expired';
|
||||||
$vip_state_label = __('已过期', 'aksrt');
|
$vip_state_label = __('已过期', 'aksrt');
|
||||||
$vip_level_number = $expired_level;
|
$vip_level_number = $expired_level;
|
||||||
$vip_level_name = (string) ($levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
$vip_level_name = (string) ($levels[$expired_level]['name_html'] ?? $levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||||
$expired_desc = (string) ($levels[$expired_level]['desc'] ?? '');
|
$expired_desc = (string) ($levels[$expired_level]['desc'] ?? '');
|
||||||
$vip_description = $expired_desc !== ''
|
$vip_description = $expired_desc !== ''
|
||||||
? $expired_desc
|
? $expired_desc
|
||||||
@@ -91,11 +95,12 @@ $status_html = '<div class="uc-vip-status" role="status">'
|
|||||||
. '<span class="uc-vip-status__state">' . esc_html($vip_state_label) . '</span>'
|
. '<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)不输出图标位:名称前不放通用证书图标。
|
||||||
. '<span class="uc-vip-status__level">' . esc_html($vip_level_name) . '</span>'
|
. ($vip_level_number > 0 ? '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>' : '')
|
||||||
|
. '<span class="uc-vip-status__level">' . wp_kses_post($vip_level_name) . '</span>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '<div class="uc-vip-status__desc">' . esc_html($vip_description) . '</div>'
|
. '<div class="uc-vip-status__desc">' . wp_kses_post($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.
|
||||||
@@ -104,15 +109,18 @@ foreach ([1, 2] as $lv) {
|
|||||||
if (empty($levels[$lv]['enabled'])) {
|
if (empty($levels[$lv]['enabled'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
$lv_name = (string) ($levels[$lv]['name_html'] ?? $levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||||
$lv_desc = (string) ($levels[$lv]['desc'] ?? '');
|
$lv_desc = (string) ($levels[$lv]['desc'] ?? '');
|
||||||
// Preserve admin equity HTML as-is (sanitize/wrap only; never re-split into items).
|
// Preserve admin equity HTML as-is (sanitize/wrap only; never re-split into items).
|
||||||
$equity_raw = (string) ($levels[$lv]['equity'] ?? '');
|
$equity_raw = (string) ($levels[$lv]['equity'] ?? '');
|
||||||
$equity = function_exists('aksrt_format_vip_equity_html')
|
$equity = function_exists('aksrt_format_vip_equity_html')
|
||||||
? aksrt_format_vip_equity_html($equity_raw)
|
? aksrt_format_vip_equity_html($equity_raw)
|
||||||
: '';
|
: '';
|
||||||
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
$cta = __('开通', 'aksrt') . wp_kses_post($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">'
|
||||||
@@ -120,9 +128,11 @@ foreach ([1, 2] as $lv) {
|
|||||||
if (str_starts_with($btn_type, 'renew_')) {
|
if (str_starts_with($btn_type, 'renew_')) {
|
||||||
$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 = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'renew';
|
||||||
} elseif ($lv > $renew_lv) {
|
} elseif ($lv > $renew_lv) {
|
||||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'upgrade';
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -130,25 +140,32 @@ foreach ([1, 2] as $lv) {
|
|||||||
if ($lv <= $level) {
|
if ($lv <= $level) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
$cta = __('升级', 'aksrt') . wp_kses_post($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 = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'renew';
|
||||||
|
}
|
||||||
|
if ($card_action === 'renew' && $renew_desc !== '') {
|
||||||
|
$card_desc = $renew_desc;
|
||||||
|
} elseif ($card_action === 'upgrade' && $upgrade_desc !== '') {
|
||||||
|
$card_desc = $upgrade_desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cards .= '<div class="aksrt-panel text-center pay-vip-info flex1 level-' . $lv . ' uc-vip-level-card">';
|
$cards .= '<div class="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>' . wp_kses_post($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">' . wp_kses_post($card_desc) . '</div>';
|
||||||
}
|
}
|
||||||
$cards .= '</div>';
|
$cards .= '</div>';
|
||||||
$cards .= $equity;
|
$cards .= $equity;
|
||||||
$cards .= '<a class="padding-lg but jb-vip' . $lv . ' radius pay-vip uc-vip-cta" href="javascript:;" vip-level="' . (int) $vip_attr . '">'
|
$cards .= '<a class="padding-lg but jb-vip' . $lv . ' radius pay-vip uc-vip-cta" href="javascript:;" vip-level="' . (int) $vip_attr . '">'
|
||||||
. '<span>' . esc_html($cta) . '</span>'
|
. '<span>' . $cta . '</span>'
|
||||||
. ' <i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></a>';
|
. ' <i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></a>';
|
||||||
$cards .= '</div></div>';
|
$cards .= '</div></div>';
|
||||||
}
|
}
|
||||||
@@ -169,10 +186,4 @@ foreach ([1, 2] as $lv) {
|
|||||||
// 下方的全宽横条;仅非会员 + 积分兑换开启 + 有可兑换套餐时输出)。
|
// 下方的全宽横条;仅非会员 + 积分兑换开启 + 有可兑换套餐时输出)。
|
||||||
echo aksrt_get_vip_points_exchange_entry_html($user_id, $level); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
|
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>
|
||||||
+14
-12
@@ -363,18 +363,20 @@ function aksrt_captcha_sanitize_provider_config(string $provider, $raw): array {
|
|||||||
*/
|
*/
|
||||||
function aksrt_captcha_scenes(): array {
|
function aksrt_captcha_scenes(): array {
|
||||||
return [
|
return [
|
||||||
'comment' => __('发表评论', 'aksrt'),
|
'comment' => __('发表评论', 'aksrt'),
|
||||||
'login' => __('用户登录', 'aksrt'),
|
'login' => __('用户登录', 'aksrt'),
|
||||||
'register' => __('用户注册', 'aksrt'),
|
'register' => __('用户注册', 'aksrt'),
|
||||||
'bind' => __('绑定第三方', 'aksrt'),
|
'bind' => __('绑定第三方', 'aksrt'),
|
||||||
'bind_phone' => __('绑定手机', 'aksrt'),
|
'bind_phone' => __('绑定手机', 'aksrt'),
|
||||||
'lostpassword' => __('找回密码', 'aksrt'),
|
'lostpassword' => __('找回密码', 'aksrt'),
|
||||||
'change_password' => __('修改密码', 'aksrt'),
|
'change_password' => __('修改密码', 'aksrt'),
|
||||||
'change_email' => __('更改邮箱', 'aksrt'),
|
'change_email' => __('更改邮箱', 'aksrt'),
|
||||||
'download' => __('资源下载', 'aksrt'),
|
'download' => __('资源下载', 'aksrt'),
|
||||||
'signin' => __('每日签到', 'aksrt'),
|
'signin' => __('每日签到', 'aksrt'),
|
||||||
'front_submit' => __('前台投稿', 'aksrt'),
|
'front_submit' => __('前台投稿', 'aksrt'),
|
||||||
'tip_user' => __('打赏用户', 'aksrt'),
|
'tip_user' => __('打赏用户', 'aksrt'),
|
||||||
|
'after_sale_apply' => __('申请售后', 'aksrt'),
|
||||||
|
'after_sale_cancel' => __('撤销售后', 'aksrt'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-105
@@ -19,30 +19,12 @@ class AIController extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function register_routes(): void {
|
public function register_routes(): void {
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/status', [
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => [$this, 'get_status'],
|
|
||||||
'permission_callback' => [$this, 'check_logged_in'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/settings', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'update_settings'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'seo_generate'],
|
'callback' => [$this, 'seo_generate'],
|
||||||
'permission_callback' => [$this, 'check_logged_in'],
|
'permission_callback' => [$this, 'check_logged_in'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'seo_suggest'],
|
|
||||||
'permission_callback' => [$this, 'check_write_assist'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'summary_suggest'],
|
'callback' => [$this, 'summary_suggest'],
|
||||||
@@ -76,66 +58,6 @@ class AIController extends ControllerBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_status(): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
|
||||||
return $this->success(['data' => [
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => 'none',
|
|
||||||
'configured' => false,
|
|
||||||
'available' => false,
|
|
||||||
'backend' => 'none',
|
|
||||||
]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(['data' => \AKSRT\Ai\AI::get_instance()->get_status()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update_settings(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
|
||||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$allowed = [
|
|
||||||
'enabled' => 'bool',
|
|
||||||
'provider' => 'string',
|
|
||||||
'api_key' => 'string',
|
|
||||||
'api_base' => 'url',
|
|
||||||
'model' => 'string',
|
|
||||||
'max_tokens' => 'int',
|
|
||||||
'rate_limit' => 'int',
|
|
||||||
];
|
|
||||||
|
|
||||||
$config = [];
|
|
||||||
foreach ($allowed as $key => $type) {
|
|
||||||
if (!isset($params[$key])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch ($type) {
|
|
||||||
case 'bool':
|
|
||||||
$config[$key] = !empty($params[$key]);
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$config[$key] = (int) $params[$key];
|
|
||||||
break;
|
|
||||||
case 'url':
|
|
||||||
$config[$key] = esc_url_raw((string) $params[$key]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$config[$key] = sanitize_text_field((string) $params[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$success = \AKSRT\Ai\AI::get_instance()->update_config($config);
|
|
||||||
if ($success) {
|
|
||||||
return $this->success([
|
|
||||||
'message' => __('AI 设置已保存', 'aksrt'),
|
|
||||||
'data' => \AKSRT\Ai\AI::get_instance()->get_status(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $this->error(__('保存失败', 'aksrt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
||||||
*/
|
*/
|
||||||
@@ -170,32 +92,6 @@ class AIController extends ControllerBase {
|
|||||||
return $this->success(['data' => $result['data']]);
|
return $this->success(['data' => $result['data']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seo_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AISEO')) {
|
|
||||||
return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
|
||||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
|
||||||
$post_id = absint($params['post_id'] ?? 0);
|
|
||||||
|
|
||||||
if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
|
|
||||||
return $this->error(__('权限不足', 'aksrt'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($title === '') {
|
|
||||||
return $this->error(__('标题不能为空', 'aksrt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = \AKSRT\Ai\AISEO::get_instance()->suggest($title, $content);
|
|
||||||
if (!empty($result['success'])) {
|
|
||||||
return $this->success(['data' => $result['data']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||||
@@ -224,7 +120,7 @@ class AIController extends ControllerBase {
|
|||||||
$ai = \AKSRT\Ai\AI::get_instance();
|
$ai = \AKSRT\Ai\AI::get_instance();
|
||||||
if (!$ai->is_available()) {
|
if (!$ai->is_available()) {
|
||||||
return $this->error(
|
return $this->error(
|
||||||
__('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'),
|
__('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||||
503
|
503
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package AKSRT
|
||||||
|
*
|
||||||
|
* 后台列表页分页条 —— 1:1 复刻「卡密管理」页的 zibpay_admin_pagenavi 结构。
|
||||||
|
*
|
||||||
|
* 支付中心各页面(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 / 提现审核 / 卡密管理)
|
||||||
|
* 原先各写一份几乎相同的翻页代码。这里统一到一处,标记沿用卡密管理那一套:
|
||||||
|
* `« ‹ 第 X 页,共 Y 页 › »` 四枚按钮 + 「每页 X 共Y」计数,没有数字页码链接。
|
||||||
|
*
|
||||||
|
* 与参考实现的两处差异,均为缺陷修复:
|
||||||
|
* 1) 基准 URL 显式以「page 参数 + 当前筛选参数」拼装。历史写法里 add_query_arg('paged', N)
|
||||||
|
* 只有两个参数时会落到 $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态
|
||||||
|
* 漂移,列条件也容易丢。
|
||||||
|
* 2) 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导。历史写法在此处会留下
|
||||||
|
* 「第 2 页 / 共 1 页」这类越界状态,四个分支一个都不命中,上一页/下一页同时停在
|
||||||
|
* '#':按钮既不禁用也点不动。
|
||||||
|
*
|
||||||
|
* 单页也渲染分页条(只要总数 > 0):与参考实现一致,运营能直接看到「每页 20 共 5」
|
||||||
|
* 这样的口径,四枚按钮全 disabled 也明确了「没有更多页」。
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin\Native;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class AdminListPagination {
|
||||||
|
|
||||||
|
/** 每页条数默认值。所有列表页一致,且不提供「每页显示」控件。 */
|
||||||
|
public const DEFAULT_PER_PAGE = 20;
|
||||||
|
|
||||||
|
/** @var bool 内联样式只输出一次 */
|
||||||
|
private static $assets_printed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染分页条。
|
||||||
|
*
|
||||||
|
* 真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成,这里只把页码
|
||||||
|
* 夹到有效范围,保证显示状态自洽。
|
||||||
|
*
|
||||||
|
* @param string $page_slug 当前 admin.php 的 page 参数。
|
||||||
|
* @param int $total_count 当前筛选条件下的记录总数。
|
||||||
|
* @param int $current_page 当前页码(未经夹取亦可)。
|
||||||
|
* @param int $per_page 每页条数。
|
||||||
|
* @param array<string, string|int> $query_args 当前筛选参数,不含 paged。
|
||||||
|
*/
|
||||||
|
public static function render(
|
||||||
|
string $page_slug,
|
||||||
|
int $total_count,
|
||||||
|
int $current_page,
|
||||||
|
int $per_page,
|
||||||
|
array $query_args = []
|
||||||
|
): void {
|
||||||
|
if ($total_count <= 0 || $per_page <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::print_assets();
|
||||||
|
|
||||||
|
$total_pages = max(1, (int) ceil($total_count / $per_page));
|
||||||
|
$current_page = min(max(1, $current_page), $total_pages);
|
||||||
|
|
||||||
|
// 显式基准 URL:不再让 add_query_arg() 只有两个参数时退回 $_SERVER['REQUEST_URI']。
|
||||||
|
$base = add_query_arg(array_merge(['page' => $page_slug], $query_args), admin_url('admin.php'));
|
||||||
|
$page_url = static function (int $page) use ($base): string {
|
||||||
|
return add_query_arg('paged', $page, $base);
|
||||||
|
};
|
||||||
|
|
||||||
|
$on_first = $current_page <= 1;
|
||||||
|
$on_last = $current_page >= $total_pages;
|
||||||
|
|
||||||
|
$first_page_url = $on_first ? '#' : $page_url(1);
|
||||||
|
$prev_page_url = $on_first ? '#' : $page_url($current_page - 1);
|
||||||
|
$next_page_url = $on_last ? '#' : $page_url($current_page + 1);
|
||||||
|
$last_page_url = $on_last ? '#' : $page_url($total_pages);
|
||||||
|
?>
|
||||||
|
<div class="tablenav bottom">
|
||||||
|
<div class="tablenav-pages">
|
||||||
|
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $per_page), esc_html((string) $total_count)); ?></span>
|
||||||
|
<span class="pagination-links">
|
||||||
|
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
|
||||||
|
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
|
||||||
|
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
|
||||||
|
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
|
||||||
|
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行内样式。
|
||||||
|
*
|
||||||
|
* 这些原生后台页面不加载主题 CSS(admin-main.css 只在 edit.php 文章列表入队),
|
||||||
|
* 所以分页条依赖的规则随标记一起内联输出。
|
||||||
|
*/
|
||||||
|
private static function print_assets(): void {
|
||||||
|
if (self::$assets_printed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$assets_printed = true;
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
||||||
|
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
||||||
|
.tablenav-pages a.disabled { pointer-events: none; }
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,9 +21,6 @@ class PayChargeCards {
|
|||||||
public const NONCE = 'aksrt_card_pass_page';
|
public const NONCE = 'aksrt_card_pass_page';
|
||||||
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
|
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
|
||||||
|
|
||||||
/** 每页条数默认值;列表页可由 ice_perpage 覆盖(无界面控件,仅供 URL / 程序化调用)。 */
|
|
||||||
public const DEFAULT_PER_PAGE = 20;
|
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
/** @var string 当前页面 URL */
|
/** @var string 当前页面 URL */
|
||||||
@@ -119,7 +116,8 @@ class PayChargeCards {
|
|||||||
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time';
|
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time';
|
||||||
$order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC';
|
$order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC';
|
||||||
$paged = max(1, (int) ($_GET['paged'] ?? 1));
|
$paged = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? self::DEFAULT_PER_PAGE)));
|
// 每页条数默认值取自共享助手;ice_perpage 仅作为 URL 参数被尊重,无界面控件。
|
||||||
|
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? AdminListPagination::DEFAULT_PER_PAGE)));
|
||||||
|
|
||||||
// 参考主题:status=0 表示未使用;本主题规范状态为 active。
|
// 参考主题:status=0 表示未使用;本主题规范状态为 active。
|
||||||
if ($status === '0' || $status === 'active') {
|
if ($status === '0' || $status === 'active') {
|
||||||
@@ -297,7 +295,7 @@ class PayChargeCards {
|
|||||||
echo '</form>';
|
echo '</form>';
|
||||||
echo '<div class="clear"></div>';
|
echo '<div class="clear"></div>';
|
||||||
|
|
||||||
$this->render_pagenavi($count_all, $per_page, $query_args);
|
AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
@@ -1059,67 +1057,18 @@ class PayChargeCards {
|
|||||||
if ($order === 'ASC') {
|
if ($order === 'ASC') {
|
||||||
$args['desc'] = 'ASC';
|
$args['desc'] = 'ASC';
|
||||||
}
|
}
|
||||||
if ($per_page !== self::DEFAULT_PER_PAGE) {
|
if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) {
|
||||||
$args['ice_perpage'] = (string) $per_page;
|
$args['ice_perpage'] = (string) $per_page;
|
||||||
}
|
}
|
||||||
return $args;
|
return $args;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。
|
|
||||||
*
|
|
||||||
* 与参考实现的两处差异,均为缺陷修复:
|
|
||||||
* 1) 页码先夹到 [1, total_pages]。历史写法在此处会出现「第 2 页 / 共 1 页」这类越界状态,
|
|
||||||
* 四个分支一个都不命中,上一页/下一页同时停在 '#',按钮既不禁用也点不动。
|
|
||||||
* 2) 链接基于显式基准 URL(page_url + 当前筛选条件),不再依赖 REQUEST_URI。
|
|
||||||
*
|
|
||||||
* @param array<string,string> $query_args 当前筛选参数(不含 paged)。
|
|
||||||
*/
|
|
||||||
private function render_pagenavi(int $total_count, int $number_per_page, array $query_args = []): void {
|
|
||||||
if ($total_count <= 0 || $number_per_page <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$total_pages = max(1, (int) ceil($total_count / $number_per_page));
|
|
||||||
$current_page = min(max(1, (int) ($_GET['paged'] ?? 1)), $total_pages);
|
|
||||||
|
|
||||||
$base = $query_args ? add_query_arg($query_args, $this->page_url) : $this->page_url;
|
|
||||||
$page_url = static function (int $page) use ($base): string {
|
|
||||||
return add_query_arg('paged', $page, $base);
|
|
||||||
};
|
|
||||||
|
|
||||||
$on_first = $current_page <= 1;
|
|
||||||
$on_last = $current_page >= $total_pages;
|
|
||||||
|
|
||||||
$first_page_url = $on_first ? '#' : $page_url(1);
|
|
||||||
$prev_page_url = $on_first ? '#' : $page_url($current_page - 1);
|
|
||||||
$next_page_url = $on_last ? '#' : $page_url($current_page + 1);
|
|
||||||
$last_page_url = $on_last ? '#' : $page_url($total_pages);
|
|
||||||
?>
|
|
||||||
<div class="tablenav bottom">
|
|
||||||
<div class="tablenav-pages">
|
|
||||||
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $number_per_page), esc_html((string) $total_count)); ?></span>
|
|
||||||
<span class="pagination-links">
|
|
||||||
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
|
|
||||||
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
|
|
||||||
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
|
|
||||||
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
|
|
||||||
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
|
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
|
||||||
private function print_assets(): void {
|
private function print_assets(): void {
|
||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||||||
.order-header .subsubsub { margin: 6px 0; }
|
.order-header .subsubsub { margin: 6px 0; }
|
||||||
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
|
||||||
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
|
||||||
.tablenav-pages a.disabled { pointer-events: none; }
|
|
||||||
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
||||||
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
||||||
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class PayCoupons {
|
|||||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
$type_labels = [
|
$type_labels = [
|
||||||
'fixed' => __('固定金额', 'aksrt'),
|
'fixed' => __('固定金额', 'aksrt'),
|
||||||
'percent' => __('折扣百分比', 'aksrt'),
|
'percent' => __('折扣百分比', 'aksrt'),
|
||||||
@@ -199,7 +199,21 @@ class PayCoupons {
|
|||||||
'offset' => ($page_num - 1) * $per_page,
|
'offset' => ($page_num - 1) * $per_page,
|
||||||
]);
|
]);
|
||||||
$items = $result['items'] ?? [];
|
$items = $result['items'] ?? [];
|
||||||
$pages = max(1, (int) ($result['total_pages'] ?? 1));
|
$total = (int) ($result['total'] ?? 0);
|
||||||
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$result = $svc->get_list([
|
||||||
|
'status' => $status,
|
||||||
|
'search' => $search,
|
||||||
|
'limit' => $per_page,
|
||||||
|
'offset' => ($page_num - 1) * $per_page,
|
||||||
|
]);
|
||||||
|
$items = $result['items'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||||
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||||
@@ -240,8 +254,16 @@ class PayCoupons {
|
|||||||
}
|
}
|
||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'status' => $status !== '' ? $status : null,
|
||||||
|
's' => $search !== '' ? $search : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->render_pagination($pages, $page_num);
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_create_coupon(): void {
|
public function handle_create_coupon(): void {
|
||||||
@@ -457,20 +479,4 @@ class PayCoupons {
|
|||||||
private function page_url(array $args = []): string {
|
private function page_url(array $args = []): string {
|
||||||
return add_query_arg(array_merge(['page' => self::PAGE_SLUG], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => self::PAGE_SLUG], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ class PayIncome {
|
|||||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
|
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
|
||||||
? \AKSRT\Payment\Order::get_valid_statuses()
|
? \AKSRT\Payment\Order::get_valid_statuses()
|
||||||
: [];
|
: [];
|
||||||
@@ -124,7 +124,31 @@ class PayIncome {
|
|||||||
$result = $svc->get_list($list_args);
|
$result = $svc->get_list($list_args);
|
||||||
$items = $result['items'] ?? [];
|
$items = $result['items'] ?? [];
|
||||||
$total = (int) ($result['total'] ?? 0);
|
$total = (int) ($result['total'] ?? 0);
|
||||||
$pages = max(1, (int) ($result['max_pages'] ?? 1));
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$list_args['offset'] = ($page_num - 1) * $per_page;
|
||||||
|
$result = $svc->get_list($list_args);
|
||||||
|
$items = $result['items'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged)。必须在下面的行循环之前取好:
|
||||||
|
// 循环里 $user_id 会被每行的 author_id 覆盖。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'status' => $status !== '' ? $status : null,
|
||||||
|
'order_status' => $order_status !== '' ? $order_status : null,
|
||||||
|
'user_id' => $user_id > 0 ? $user_id : null,
|
||||||
|
'post_id' => $post_id > 0 ? $post_id : null,
|
||||||
|
'currency' => $currency !== '' ? $currency : null,
|
||||||
|
'order_number' => $order_number !== '' ? $order_number : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||||
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||||
@@ -164,7 +188,7 @@ class PayIncome {
|
|||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->render_pagination($pages, $page_num);
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,19 +280,4 @@ class PayIncome {
|
|||||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,17 @@ class PayMenus {
|
|||||||
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'after_sales' => [
|
||||||
|
'slug' => 'aksrt_after_sales',
|
||||||
|
'title' => __('售后服务', 'aksrt'),
|
||||||
|
'callback' => static function (): void {
|
||||||
|
if (class_exists('AKSRT\Admin\Native\PayOrders')) {
|
||||||
|
PayOrders::get_instance()->render_after_sales_admin_page();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
||||||
|
},
|
||||||
|
],
|
||||||
'products' => [
|
'products' => [
|
||||||
'slug' => 'aksrt_pay_products',
|
'slug' => 'aksrt_pay_products',
|
||||||
'title' => __('付费文章', 'aksrt'),
|
'title' => __('付费文章', 'aksrt'),
|
||||||
|
|||||||
+429
-136
@@ -11,6 +11,7 @@ class PayOrders {
|
|||||||
|
|
||||||
public const CAPABILITY = 'manage_options';
|
public const CAPABILITY = 'manage_options';
|
||||||
public const PAGE_SLUG = 'aksrt_orders';
|
public const PAGE_SLUG = 'aksrt_orders';
|
||||||
|
public const PAGE_AFTER_SALES_SLUG = 'aksrt_after_sales';
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ class PayOrders {
|
|||||||
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
|
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
|
||||||
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
|
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
|
||||||
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
|
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
|
||||||
add_action('admin_post_aksrt_after_sale_review', [$this, 'handle_after_sale_review']);
|
add_action('admin_post_aksrt_pay_refund_order', [$this, 'handle_refund_order']);
|
||||||
|
add_action('admin_post_aksrt_after_sale_update', [$this, 'handle_after_sale_status']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render_orders_page(): void {
|
public function render_orders_page(): void {
|
||||||
@@ -42,20 +44,6 @@ class PayOrders {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'orders';
|
|
||||||
if (!in_array($tab, ['stats', 'orders', 'after-sales'], true)) $tab = 'orders';
|
|
||||||
$this->render_workbench_tabs($tab);
|
|
||||||
if ($tab === 'stats') {
|
|
||||||
$this->render_statistics_page();
|
|
||||||
$this->render_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ($tab === 'after-sales') {
|
|
||||||
$this->render_after_sales_page();
|
|
||||||
$this->render_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : '';
|
$detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : '';
|
||||||
if ($detail_order !== '') {
|
if ($detail_order !== '') {
|
||||||
$this->render_order_detail_page($detail_order);
|
$this->render_order_detail_page($detail_order);
|
||||||
@@ -89,7 +77,7 @@ class PayOrders {
|
|||||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
if (!array_key_exists($product_type, $type_tabs)) {
|
if (!array_key_exists($product_type, $type_tabs)) {
|
||||||
$product_type = '';
|
$product_type = '';
|
||||||
@@ -130,20 +118,38 @@ class PayOrders {
|
|||||||
? \AKSRT\Core\DomainModel::order_status_labels()
|
? \AKSRT\Core\DomainModel::order_status_labels()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$args['offset'] = ($page_num - 1) * $per_page;
|
||||||
|
$items = $repo->get_orders_admin($args, $search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前筛选参数(不含 paged):标签页链接与翻页链接共用同一份,避免两处漂移。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'product_type' => $product_type !== '' ? $product_type : null,
|
||||||
|
'product_id' => $product_id > 0 ? $product_id : null,
|
||||||
|
'user_id' => $user_id > 0 ? $user_id : null,
|
||||||
|
'payment_method' => $pay_method !== '' ? $pay_method : null,
|
||||||
|
'status' => ($status !== '' && $status !== 'all') ? $status : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
's' => $search !== '' ? $search : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
// Type tabs.
|
// Type tabs.
|
||||||
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
|
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
|
||||||
$tab_bits = [];
|
$tab_bits = [];
|
||||||
foreach ($type_tabs as $tkey => $tlabel) {
|
foreach ($type_tabs as $tkey => $tlabel) {
|
||||||
$url = $this->page_url('aksrt_orders', array_filter([
|
$url = $this->page_url('aksrt_orders', array_filter(
|
||||||
'product_type' => $tkey !== '' ? $tkey : null,
|
['product_type' => $tkey !== '' ? $tkey : null] + $query_args,
|
||||||
'product_id' => $product_id > 0 ? $product_id : null,
|
static function ($value): bool {
|
||||||
'user_id' => $user_id > 0 ? $user_id : null,
|
return $value !== null;
|
||||||
'payment_method' => $pay_method !== '' ? $pay_method : null,
|
}
|
||||||
'status' => ($status !== '' && $status !== 'all') ? $status : null,
|
));
|
||||||
'date_from' => $date_from !== '' ? $date_from : null,
|
|
||||||
'date_to' => $date_to !== '' ? $date_to : null,
|
|
||||||
's' => $search !== '' ? $search : null,
|
|
||||||
]));
|
|
||||||
$cls = ($product_type === $tkey) ? 'current' : '';
|
$cls = ($product_type === $tkey) ? 'current' : '';
|
||||||
$tab_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
|
$tab_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
|
||||||
}
|
}
|
||||||
@@ -154,16 +160,12 @@ class PayOrders {
|
|||||||
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
|
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
|
||||||
$m_bits = [];
|
$m_bits = [];
|
||||||
foreach ($method_tabs as $mkey => $mlabel) {
|
foreach ($method_tabs as $mkey => $mlabel) {
|
||||||
$url = $this->page_url('aksrt_orders', array_filter([
|
$url = $this->page_url('aksrt_orders', array_filter(
|
||||||
'product_type' => $product_type !== '' ? $product_type : null,
|
['payment_method' => $mkey !== '' ? $mkey : null] + $query_args,
|
||||||
'product_id' => $product_id > 0 ? $product_id : null,
|
static function ($value): bool {
|
||||||
'user_id' => $user_id > 0 ? $user_id : null,
|
return $value !== null;
|
||||||
'payment_method' => $mkey !== '' ? $mkey : null,
|
}
|
||||||
'status' => ($status !== '' && $status !== 'all') ? $status : null,
|
));
|
||||||
'date_from' => $date_from !== '' ? $date_from : null,
|
|
||||||
'date_to' => $date_to !== '' ? $date_to : null,
|
|
||||||
's' => $search !== '' ? $search : null,
|
|
||||||
]));
|
|
||||||
$cls = ($pay_method === $mkey) ? 'current' : '';
|
$cls = ($pay_method === $mkey) ? 'current' : '';
|
||||||
$m_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
|
$m_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
|
||||||
}
|
}
|
||||||
@@ -267,6 +269,15 @@ class PayOrders {
|
|||||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
|
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
|
||||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||||
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
||||||
|
// 已支付是已退款的唯一前置状态;退款仍需落到一条进行中的售后工单上。
|
||||||
|
if ($status_key === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||||
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认将该订单标记为已退款?不可恢复。', 'aksrt')) . '\');">';
|
||||||
|
echo '<input type="hidden" name="action" value="aksrt_pay_refund_order" />';
|
||||||
|
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||||
|
wp_nonce_field('aksrt_pay_refund_order_' . $order_number);
|
||||||
|
submit_button(__('退款', 'aksrt'), 'small', '', false);
|
||||||
|
echo '</form>';
|
||||||
|
}
|
||||||
$this->render_order_quick_status_buttons($order_number, $status_key);
|
$this->render_order_quick_status_buttons($order_number, $status_key);
|
||||||
if ($status_key === 'closed') {
|
if ($status_key === 'closed') {
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||||
@@ -282,19 +293,36 @@ class PayOrders {
|
|||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
|
||||||
if ($pages > 1) {
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
$this->render_footer();
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
}
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
/**
|
||||||
'next_text' => '»',
|
* 售后服务子菜单(aksrt_after_sales):原订单明细页「售后」Tab 独立成页。
|
||||||
'total' => $pages,
|
*/
|
||||||
'current' => $page_num,
|
public function render_after_sales_admin_page(): void {
|
||||||
]);
|
$this->ensure_cap();
|
||||||
echo '</div></div>';
|
$this->render_header(__('售后服务', 'aksrt'));
|
||||||
|
|
||||||
|
if (!class_exists('\\AKSRT\\Payment\\AfterSaleService')) {
|
||||||
|
echo '<div class="notice notice-error"><p>' . esc_html__('售后服务不可用。', 'aksrt') . '</p></div>';
|
||||||
|
$this->render_footer();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$policy = \AKSRT\Settings\PaymentRuntime::get_after_sale_settings();
|
||||||
|
$policy_bits = [];
|
||||||
|
$policy_bits[] = __('售后功能', 'aksrt') . ':' . ($policy['enabled'] ? __('开启', 'aksrt') : __('关闭', 'aksrt'));
|
||||||
|
$limit_days = (int) ($policy['time_limit'] ?? 0);
|
||||||
|
$policy_bits[] = __('申请售后时效', 'aksrt') . ':' . ($limit_days > 0 ? sprintf(__('%d 天', 'aksrt'), $limit_days) : __('不限制', 'aksrt'));
|
||||||
|
$reason_names = array_column((array) ($policy['reasons'] ?? []), 'name');
|
||||||
|
if ($reason_names) $policy_bits[] = __('预置原因', 'aksrt') . ':' . implode('、', $reason_names);
|
||||||
|
$material_names = array_values(array_filter(array_map('trim', (array) ($policy['materials'] ?? []))));
|
||||||
|
if ($material_names) $policy_bits[] = __('申请所需材料', 'aksrt') . ':' . implode('、', $material_names);
|
||||||
|
echo '<p class="description">' . esc_html(implode(' ', $policy_bits)) . '</p>';
|
||||||
|
|
||||||
|
$this->render_after_sales_page();
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +351,13 @@ class PayOrders {
|
|||||||
|
|
||||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||||
|
|
||||||
|
// 售后记录区块:工单卡片 + 处理记录列表(与售后面板共用 .aksrt-as-log 样式)。
|
||||||
|
echo '<style>'
|
||||||
|
. '.aksrt-as-ticket{padding:10px 0;border-bottom:1px solid #f0f0f1;}'
|
||||||
|
. '.aksrt-as-ticket:last-child{padding-bottom:0;border-bottom:0;}'
|
||||||
|
. '.aksrt-as-ticket__head{margin:0 0 4px;}'
|
||||||
|
. $this->after_sale_log_css()
|
||||||
|
. '</style>';
|
||||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||||
$rows = [
|
$rows = [
|
||||||
__('订单号', 'aksrt') => $order_number,
|
__('订单号', 'aksrt') => $order_number,
|
||||||
@@ -343,12 +378,17 @@ class PayOrders {
|
|||||||
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
||||||
if ($after_sales) {
|
if ($after_sales) {
|
||||||
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
||||||
$status_labels = ['pending' => '待审核', 'processing' => '处理中', 'approved' => '已通过', 'rejected' => '已拒绝', 'cancelled' => '已取消', 'completed' => '已完成'];
|
$status_labels = $this->after_sale_status_labels();
|
||||||
foreach ($after_sales as $application) {
|
foreach ($after_sales as $application) {
|
||||||
echo '<p><strong>' . esc_html($status_labels[(string) ($application['status'] ?? '')] ?? (string) ($application['status'] ?? '')) . '</strong> · ' . esc_html((string) ($application['refund_amount'] ?? '')) . '<br>' . esc_html((string) ($application['reason'] ?? ''));
|
$as_status = (string) ($application['status'] ?? '');
|
||||||
if (!empty($application['admin_note'])) echo '<br>' . esc_html__('处理备注:', 'aksrt') . esc_html((string) $application['admin_note']);
|
echo '<div class="aksrt-as-ticket is-' . esc_attr($as_status) . '">';
|
||||||
if (!empty($application['gateway_result'])) echo '<br><code>' . esc_html((string) $application['gateway_result']) . '</code>';
|
echo '<p class="aksrt-as-ticket__head"><strong>' . esc_html($status_labels[$as_status] ?? $as_status) . '</strong>'
|
||||||
echo '</p>';
|
. ' <span class="description">' . esc_html((string) ($application['refund_amount'] ?? '')) . '</span>'
|
||||||
|
. ' <span class="description">' . esc_html((string) ($application['created_at'] ?? '')) . '</span></p>';
|
||||||
|
echo '<p>' . nl2br(esc_html((string) ($application['reason'] ?? ''))) . '</p>';
|
||||||
|
// 处理记录:后台每一次提交都留一条(状态 + 备注 + 时间),与售后面板同款标记。
|
||||||
|
echo $this->render_after_sale_log((array) ($application['handling_log'] ?? []), $status_labels); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
echo '</div>';
|
||||||
}
|
}
|
||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
@@ -372,90 +412,320 @@ class PayOrders {
|
|||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_workbench_tabs(string $active): void {
|
|
||||||
echo '<nav class="nav-tab-wrapper" style="margin:12px 0 18px;">';
|
|
||||||
foreach (['stats' => __('统计', 'aksrt'), 'orders' => __('订单', 'aksrt'), 'after-sales' => __('售后', 'aksrt')] as $key => $label) {
|
|
||||||
$url = $this->page_url(self::PAGE_SLUG, ['tab' => $key]);
|
|
||||||
echo '<a class="nav-tab ' . ($active === $key ? 'nav-tab-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
|
|
||||||
}
|
|
||||||
echo '</nav>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function render_statistics_page(): void {
|
|
||||||
$stats = \AKSRT\Payment\OrderRepository::get_instance()->get_admin_order_stats([
|
|
||||||
'date_from' => isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '',
|
|
||||||
'date_to' => isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '',
|
|
||||||
]);
|
|
||||||
echo '<form method="get" style="margin-bottom:16px;"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" /><input type="hidden" name="tab" value="stats" />';
|
|
||||||
echo '<label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr((string) ($_GET['date_from'] ?? '')) . '" /></label> ';
|
|
||||||
echo '<label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr((string) ($_GET['date_to'] ?? '')) . '" /></label> ';
|
|
||||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
|
||||||
echo '</form><div style="display:flex;flex-wrap:wrap;gap:12px;">';
|
|
||||||
foreach (['total' => __('订单总数', 'aksrt'), 'pending' => __('待支付', 'aksrt'), 'paid' => __('已支付', 'aksrt'), 'refunded' => __('已退款', 'aksrt'), 'closed' => __('已关闭', 'aksrt'), 'revenue' => __('实收金额', 'aksrt'), 'after_sale_pending' => __('售后待审核', 'aksrt')] as $key => $label) {
|
|
||||||
$this->stat_card($label, (string) ($stats[$key] ?? 0), $this->page_url(self::PAGE_SLUG, ['tab' => 'orders']));
|
|
||||||
}
|
|
||||||
echo '</div><h2>' . esc_html__('按商品类型统计', 'aksrt') . '</h2><table class="widefat striped" style="max-width:640px;"><tbody>';
|
|
||||||
foreach ((array) ($stats['by_type'] ?? []) as $type => $count) echo '<tr><th>' . esc_html(\AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type) . '</th><td>' . esc_html((string) $count) . '</td></tr>';
|
|
||||||
echo '</tbody></table>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function render_after_sales_page(): void {
|
private function render_after_sales_page(): void {
|
||||||
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
||||||
$status = isset($_GET['after_sale_status']) ? sanitize_key((string) wp_unslash($_GET['after_sale_status'])) : '';
|
$status = isset($_GET['after_sale_status']) ? sanitize_key((string) wp_unslash($_GET['after_sale_status'])) : '';
|
||||||
|
// admin_list() 会按状态常量白名单过滤,count() 不会。这里先归一一次,
|
||||||
|
// 否则手改 URL 传个野状态会变成「count 为 0 而 list 返回全部」,分页条直接说谎。
|
||||||
|
if (!in_array($status, $this->after_sale_statuses(), true)) {
|
||||||
|
$status = '';
|
||||||
|
}
|
||||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$date_from = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from) ? $date_from : '';
|
$date_from = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from) ? $date_from : '';
|
||||||
$date_to = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to) ? $date_to : '';
|
$date_to = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to) ? $date_to : '';
|
||||||
$filter_args = ['status' => $status, 'date_from' => $date_from, 'date_to' => $date_to];
|
$filter_args = ['status' => $status, 'date_from' => $date_from, 'date_to' => $date_to];
|
||||||
$items = $service->admin_list($filter_args + ['limit' => 100]);
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" /><input type="hidden" name="tab" value="after-sales" />';
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
|
$items = $service->admin_list($filter_args + ['limit' => $per_page, 'offset' => ($page_num - 1) * $per_page]);
|
||||||
|
$total = $service->count($filter_args);
|
||||||
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$items = $service->admin_list($filter_args + ['limit' => $per_page, 'offset' => ($page_num - 1) * $per_page]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_AFTER_SALES_SLUG) . '" />';
|
||||||
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
|
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
|
||||||
foreach ([\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成'] as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
foreach ($this->after_sale_status_labels() as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||||
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
|
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
|
||||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||||
echo '</form><table class="widefat striped"><thead><tr><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
|
echo '</form>';
|
||||||
if (!$items) echo '<tr><td colspan="7">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
echo '<style>'
|
||||||
|
. '.aksrt-as-detail-row>td{padding:0 0 6px;}'
|
||||||
|
. '.aksrt-as-detail-row[hidden]{display:none;}'
|
||||||
|
. '.aksrt-as-detail{margin:0 12px;padding:14px 18px;border:1px solid #dcdcde;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,.04);}'
|
||||||
|
. '.aksrt-as-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 28px;}'
|
||||||
|
. '.aksrt-as-field .aksrt-as-label{display:block;color:#646970;font-size:12px;margin-bottom:2px;}'
|
||||||
|
. '.aksrt-as-field .aksrt-as-value{display:block;word-break:break-word;}'
|
||||||
|
. '.aksrt-as-field{min-width:0;}'
|
||||||
|
. '.aksrt-as-thumb{width:48px;height:48px;object-fit:cover;border-radius:4px;margin:0 4px 4px 0;vertical-align:middle;}'
|
||||||
|
. '.aksrt-as-detail .aksrt-as-thumb{width:80px;height:80px;}'
|
||||||
|
. '.aksrt-as-review{margin-top:14px;padding-top:12px;border-top:1px solid #dcdcde;display:flex;flex-wrap:wrap;align-items:flex-end;gap:12px 16px;}'
|
||||||
|
. '.aksrt-as-review-head{flex:1 1 100%;}'
|
||||||
|
. '.aksrt-as-review-main{flex:1 1 460px;min-width:0;display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 12px;}'
|
||||||
|
. '.aksrt-as-review-main p{margin:0;}'
|
||||||
|
. '.aksrt-status-select{min-width:150px;}'
|
||||||
|
. '.aksrt-as-refund-flag{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;margin:0;}'
|
||||||
|
. '.aksrt-as-review textarea{flex:1 1 240px;min-width:0;box-sizing:border-box;margin:0;}'
|
||||||
|
. '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;}'
|
||||||
|
. '.aksrt-as-field--wide{grid-column:1/-1;}'
|
||||||
|
. $this->after_sale_log_css()
|
||||||
|
. '@media (min-width:1500px){.aksrt-as-detail-grid{grid-template-columns:repeat(5,minmax(0,1fr));}}'
|
||||||
|
. '@media (max-width:600px){.aksrt-as-detail-grid{grid-template-columns:minmax(0,1fr);}}'
|
||||||
|
. '</style>';
|
||||||
|
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>申请资料</th><th>申请说明</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
|
||||||
|
if (!$items) echo '<tr><td colspan="9">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
||||||
|
$status_labels = $this->after_sale_status_labels();
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
||||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($number);
|
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($number);
|
||||||
$type = $order ? \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? [])) : '';
|
$type = $order ? \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? [])) : '';
|
||||||
$type_label = \AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type;
|
$type_label = \AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type;
|
||||||
$status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成'];
|
$product_name = $order ? (string) (\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '';
|
||||||
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
||||||
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
|
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
|
||||||
echo '<tr><td>' . esc_html((string) $id) . '<br><small>' . esc_html((string) ($item['created_at'] ?? '')) . '</small></td><td><a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['tab' => 'orders', 'order' => $number])) . '"><code>' . esc_html($number) . '</code></a><br><span class="description">' . esc_html($type_label) . ($order ? ' · ' . esc_html(\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '') . '</span></td><td>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '</td><td>' . esc_html((string) $item['reason']) . '</td><td>' . esc_html($refund_amount) . '</td><td>' . esc_html($status_labels[(string) $item['status']] ?? (string) $item['status']) . '</td><td>';
|
$apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null;
|
||||||
if ((string) $item['status'] === 'pending') foreach (['approve' => '通过', 'reject' => '拒绝'] as $decision => $label) {
|
if ($apply_form) {
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;vertical-align:top;margin-right:4px;"><input type="hidden" name="action" value="aksrt_after_sale_review" /><input type="hidden" name="after_sale_id" value="' . esc_attr((string) $id) . '" /><input type="hidden" name="decision" value="' . esc_attr($decision) . '" />'; wp_nonce_field('aksrt_after_sale_review_' . $id); echo '<textarea name="admin_note" rows="2" placeholder="' . esc_attr__('管理员备注', 'aksrt') . '"></textarea><br><button class="button button-small" type="submit">' . esc_html($label) . '</button></form>';
|
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
||||||
}
|
$material_lines = [];
|
||||||
if ((string) $item['status'] === 'processing') {
|
foreach ((array) ($apply_form['materials'] ?? []) as $m) {
|
||||||
$gateway_result = json_decode((string) ($item['gateway_result'] ?? ''), true);
|
$ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? ''));
|
||||||
$manual_refund = is_array($gateway_result) && sanitize_key((string) ($gateway_result['status'] ?? '')) === 'manual';
|
if ($ml === '' && $mv === '') continue;
|
||||||
foreach (($manual_refund ? ['complete' => '确认线下退款完成', 'reject' => '确认未退款并拒绝'] : ['approve' => '同步退款状态']) as $decision => $label) {
|
$material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv);
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;margin-right:4px;"><input type="hidden" name="action" value="aksrt_after_sale_review" /><input type="hidden" name="after_sale_id" value="' . esc_attr((string) $id) . '" /><input type="hidden" name="decision" value="' . esc_attr($decision) . '" />'; wp_nonce_field('aksrt_after_sale_review_' . $id); echo '<button class="button button-small" type="submit">' . esc_html__($label, 'aksrt') . '</button></form>';
|
|
||||||
}
|
}
|
||||||
|
$materials_cell = $material_lines ? implode('<br>', $material_lines) : '—';
|
||||||
|
$desc_text = trim((string) ($apply_form['description'] ?? ''));
|
||||||
|
} else {
|
||||||
|
// 老数据:融合串原样展示在原因列
|
||||||
|
$reason_cell = nl2br(esc_html((string) $item['reason']));
|
||||||
|
$materials_cell = '—';
|
||||||
|
$desc_text = '';
|
||||||
}
|
}
|
||||||
if (!empty($item['admin_note'])) echo '<small>' . esc_html__('备注:', 'aksrt') . esc_html((string) $item['admin_note']) . '</small><br>';
|
$image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string'));
|
||||||
if (!empty($item['gateway_result'])) {
|
$thumbs = [];
|
||||||
$result_data = json_decode((string) $item['gateway_result'], true);
|
foreach ($image_urls as $url) {
|
||||||
$result_text = is_array($result_data) ? implode(' · ', array_filter([(string) ($result_data['status'] ?? ''), (string) ($result_data['message'] ?? ''), !empty($result_data['refund_id']) ? '退款单号 ' . (string) $result_data['refund_id'] : ''])) : (string) $item['gateway_result'];
|
// 不写原生 loading 懒加载属性:lazyload 契约测试全仓库禁 loading=lazy/eager,
|
||||||
echo '<small>' . esc_html__('渠道结果:', 'aksrt') . ' ' . esc_html($result_text) . '</small>';
|
// 且后台不加载 lazySizes 运行时,写了也不会被主题那套接管。
|
||||||
|
// 与 UsersMenusVerify 的后台缩略图保持一致(光 <img src>)。
|
||||||
|
$thumbs[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener"><img src="' . esc_url($url) . '" alt="" class="aksrt-as-thumb" /></a>';
|
||||||
}
|
}
|
||||||
echo '</td></tr>';
|
// 举证图片并入申请说明:缩略图展示在描述文字下方,不再单独成列。
|
||||||
|
$desc_cell = ($desc_text !== '' ? nl2br(esc_html($desc_text)) : '') . ($thumbs ? '<div class="aksrt-as-desc-thumbs" style="margin-top:6px;">' . implode('', $thumbs) . '</div>' : '');
|
||||||
|
if ($desc_cell === '') $desc_cell = '—';
|
||||||
|
$status_label = $status_labels[(string) $item['status']] ?? (string) $item['status'];
|
||||||
|
$order_refunded = is_array($order) && (string) ($order['status'] ?? '') === \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED;
|
||||||
|
// 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与状态面板
|
||||||
|
echo '<tr class="aksrt-as-row">';
|
||||||
|
echo '<td>' . esc_html((string) $id) . '<br><small>' . esc_html((string) ($item['created_at'] ?? '')) . '</small></td>';
|
||||||
|
echo '<td><a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['order' => $number])) . '"><code>' . esc_html($number) . '</code></a><br><span class="description">' . esc_html($type_label) . ($product_name !== '' ? ' · ' . esc_html($product_name) : '') . '</span></td>';
|
||||||
|
echo '<td>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '</td>';
|
||||||
|
echo '<td>' . $reason_cell . '</td>';
|
||||||
|
echo '<td>' . $materials_cell . '</td>';
|
||||||
|
echo '<td>' . $desc_cell . '</td>';
|
||||||
|
echo '<td>' . esc_html($refund_amount) . '</td>';
|
||||||
|
echo '<td>' . esc_html($status_label) . '</td>';
|
||||||
|
echo '<td><a href="#" class="aksrt-toggle-row-actions" data-target="aksrt-as-detail-' . esc_attr((string) $id) . '" aria-expanded="false" aria-controls="aksrt-as-detail-' . esc_attr((string) $id) . '">' . esc_html__('操作', 'aksrt') . '</a></td>';
|
||||||
|
echo '</tr>';
|
||||||
|
echo '<tr class="aksrt-as-detail-row" id="aksrt-as-detail-' . esc_attr((string) $id) . '" hidden><td colspan="9">';
|
||||||
|
$detail_fields = [
|
||||||
|
__('订单号', 'aksrt') => '<a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['order' => $number])) . '"><code>' . esc_html($number) . '</code></a>',
|
||||||
|
__('商品', 'aksrt') => esc_html($type_label . ($product_name !== '' ? ' · ' . $product_name : '')),
|
||||||
|
__('用户', 'aksrt') => esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid),
|
||||||
|
__('申请时间', 'aksrt') => esc_html((string) ($item['created_at'] ?? '')),
|
||||||
|
__('退款金额', 'aksrt') => esc_html($refund_amount),
|
||||||
|
__('当前状态', 'aksrt') => esc_html($status_label),
|
||||||
|
__('售后原因', 'aksrt') => $reason_cell,
|
||||||
|
__('申请资料', 'aksrt') => $materials_cell,
|
||||||
|
];
|
||||||
|
echo '<div class="aksrt-as-detail"><div class="aksrt-as-detail-grid">';
|
||||||
|
foreach ($detail_fields as $field_label => $field_value) {
|
||||||
|
echo '<span class="aksrt-as-field"><span class="aksrt-as-label">' . esc_html($field_label) . '</span><span class="aksrt-as-value">' . $field_value . '</span></span>';
|
||||||
|
}
|
||||||
|
// 处理备注:单独占整行,放在申请说明上方;每次提交一条,含状态与时间。
|
||||||
|
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('处理备注', 'aksrt') . '</span><span class="aksrt-as-value">'
|
||||||
|
. $this->render_after_sale_log((array) ($item['handling_log'] ?? []), $status_labels) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
. '</span></span>';
|
||||||
|
// 举证图片并入申请说明,单独占整行展示。
|
||||||
|
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('申请说明', 'aksrt') . '</span><span class="aksrt-as-value">' . $desc_cell . '</span></span>';
|
||||||
|
echo '</div>';
|
||||||
|
$status_options = '';
|
||||||
|
$current_status = (string) $item['status'];
|
||||||
|
foreach ($status_labels as $key => $label) {
|
||||||
|
$status_options .= '<option value="' . esc_attr($key) . '"' . selected($current_status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||||
|
}
|
||||||
|
if (!isset($status_labels[$current_status])) {
|
||||||
|
// 历史状态(如已删除的 approved)不在可选项内:如实呈现且禁用,防止误提交。
|
||||||
|
$status_options .= '<option value="' . esc_attr($current_status) . '" selected="selected" disabled>' . esc_html($current_status) . '</option>';
|
||||||
|
}
|
||||||
|
echo '<div class="aksrt-as-review">';
|
||||||
|
echo '<p class="aksrt-as-review-head" style="margin:0;"><strong>' . esc_html__('售后状态', 'aksrt') . '</strong> <span class="description">' . esc_html__('选择目标状态后点击确认;取消仅收起面板。', 'aksrt') . '</span></p>';
|
||||||
|
echo '<div class="aksrt-as-review-main">';
|
||||||
|
echo '<p><select class="aksrt-status-select">' . $status_options . '</select></p>';
|
||||||
|
echo '<textarea class="aksrt-review-note" rows="2" placeholder="' . esc_attr__('填写处理备注(可留空)', 'aksrt') . '"></textarea>';
|
||||||
|
echo '</div>';
|
||||||
|
// 勾选后在确认时把关联订单推到已退款;订单本已退款时呈现为不可改的已选状态
|
||||||
|
echo '<label class="aksrt-as-refund-flag"><input type="checkbox" class="aksrt-mark-order-refunded" value="1"'
|
||||||
|
. ($order_refunded ? ' checked="checked" disabled' : '')
|
||||||
|
. ' /> ' . esc_html__('标记订单已退款', 'aksrt') . '</label>';
|
||||||
|
echo '<div class="aksrt-as-review-actions">';
|
||||||
|
echo '<button type="button" class="button aksrt-after-sale-cancel">' . esc_html__('取消', 'aksrt') . '</button>';
|
||||||
|
echo '<button type="button" class="button button-primary aksrt-after-sale-confirm"'
|
||||||
|
. ' data-id="' . esc_attr((string) $id) . '"'
|
||||||
|
. ' data-nonce="' . esc_attr(wp_create_nonce('aksrt_after_sale_status_' . $id)) . '">'
|
||||||
|
. esc_html__('确认', 'aksrt') . '</button>';
|
||||||
|
echo '</div></div>';
|
||||||
|
echo '</div></td></tr>';
|
||||||
}
|
}
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'after_sale_status' => $status !== '' ? $status : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
AdminListPagination::render(self::PAGE_AFTER_SALES_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
|
|
||||||
|
// 事件委托:点击「操作」切换手风琴详情;「取消」仅收起面板;「确认」以 POST 提交选中的售后状态
|
||||||
|
echo '<script>(function(){'
|
||||||
|
. 'var postUrl=' . wp_json_encode(esc_url_raw(admin_url('admin-post.php'))) . ';'
|
||||||
|
. 'var tConfirmStatus=' . wp_json_encode(__('确认将售后状态设置为', 'aksrt')) . ';'
|
||||||
|
. 'function closeAllDetails(){'
|
||||||
|
. 'document.querySelectorAll(".aksrt-as-detail-row").forEach(function(x){x.setAttribute("hidden","");});'
|
||||||
|
. 'document.querySelectorAll(".aksrt-toggle-row-actions").forEach(function(t){t.setAttribute("aria-expanded","false");});'
|
||||||
|
. '}'
|
||||||
|
. 'document.addEventListener("click",function(e){'
|
||||||
|
. 'var el=e.target;'
|
||||||
|
. 'var t=el&&el.closest?el.closest(".aksrt-toggle-row-actions"):null;'
|
||||||
|
. 'if(t){e.preventDefault();'
|
||||||
|
. 'var p=document.getElementById(t.getAttribute("data-target"));if(!p)return;'
|
||||||
|
. 'var willOpen=p.hasAttribute("hidden");closeAllDetails();'
|
||||||
|
. 'if(willOpen){p.removeAttribute("hidden");t.setAttribute("aria-expanded","true");'
|
||||||
|
. 'var s=p.querySelector(".aksrt-status-select");if(s)s.focus();}'
|
||||||
|
. 'return;}'
|
||||||
|
. 'var c=el&&el.closest?el.closest(".aksrt-after-sale-cancel"):null;'
|
||||||
|
. 'if(c){e.preventDefault();closeAllDetails();return;}'
|
||||||
|
. 'var a=el&&el.closest?el.closest(".aksrt-after-sale-confirm"):null;'
|
||||||
|
. 'if(!a)return;e.preventDefault();'
|
||||||
|
. 'var panel=a.closest(".aksrt-as-detail-row");'
|
||||||
|
. 'var sel=panel?panel.querySelector(".aksrt-status-select"):null;'
|
||||||
|
. 'var noteEl=panel?panel.querySelector(".aksrt-review-note"):null;'
|
||||||
|
. 'var refundEl=panel?panel.querySelector(".aksrt-mark-order-refunded"):null;'
|
||||||
|
. 'var markRefund=!!(refundEl&&refundEl.checked&&!refundEl.disabled);'
|
||||||
|
. 'var opt=sel?sel.options[sel.selectedIndex]:null;'
|
||||||
|
. 'if(!sel||!opt||opt.disabled){alert(' . wp_json_encode(__('请选择售后状态。', 'aksrt')) . ');return;}'
|
||||||
|
. 'var confirmText=tConfirmStatus+"「"+opt.text+"」?";'
|
||||||
|
. 'if(markRefund)confirmText+=' . wp_json_encode(__('同时会将关联订单标记为已退款。', 'aksrt')) . ';'
|
||||||
|
. 'if(!confirm(confirmText))return;'
|
||||||
|
. 'var f=document.createElement("form");f.method="post";f.action=postUrl;'
|
||||||
|
. 'var fields={action:"aksrt_after_sale_update",after_sale_id:a.getAttribute("data-id"),after_sale_status:sel.value,mark_order_refunded:markRefund?"1":"",_wpnonce:a.getAttribute("data-nonce"),admin_note:noteEl?noteEl.value:""};'
|
||||||
|
. 'Object.keys(fields).forEach(function(k){var i=document.createElement("input");i.type="hidden";i.name=k;i.value=fields[k];f.appendChild(i);});'
|
||||||
|
. 'document.body.appendChild(f);f.submit();'
|
||||||
|
. '});'
|
||||||
|
. '})();</script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_after_sale_review(): void {
|
/** 售后状态白名单:admin_list() 据此过滤,count() 不校验,故查询前先按它归一。 */
|
||||||
|
private function after_sale_statuses(): array {
|
||||||
|
return [
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PENDING,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 售后状态中文标签:订单明细页与售后面板(含筛选下拉)共用,取代两处内联数组。 */
|
||||||
|
/** 处理记录列表样式:两个后台页面各自输出同一套规则,不依赖对方加载。 */
|
||||||
|
private function after_sale_log_css(): string {
|
||||||
|
return '.aksrt-as-log{display:grid;gap:5px;margin:6px 0 0;padding:0;list-style:none;}'
|
||||||
|
. '.aksrt-as-log__item{display:flex;flex-wrap:wrap;gap:3px 10px;align-items:baseline;font-size:12px;line-height:1.7;}'
|
||||||
|
. '.aksrt-as-log__time{color:#646970;font-variant-numeric:tabular-nums;}'
|
||||||
|
. '.aksrt-as-log__status{font-weight:600;}'
|
||||||
|
. '.aksrt-as-log__item.is-pending .aksrt-as-log__status{color:#b26200;}'
|
||||||
|
. '.aksrt-as-log__item.is-processing .aksrt-as-log__status{color:#2271b1;}'
|
||||||
|
. '.aksrt-as-log__item.is-completed .aksrt-as-log__status{color:#007017;}'
|
||||||
|
. '.aksrt-as-log__item.is-rejected .aksrt-as-log__status,.aksrt-as-log__item.is-cancelled .aksrt-as-log__status{color:#b32d2e;}'
|
||||||
|
. '.aksrt-as-log__note{flex:1 1 100%;color:#3c434a;}';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function after_sale_status_labels(): array {
|
||||||
|
return [
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理记录列表:每一次状态提交一条「时间 + 状态 + 备注」,备注为空也留痕。
|
||||||
|
* 后台订单明细页与售后面板共用同一套标记与样式。
|
||||||
|
*
|
||||||
|
* @param list<array<string,mixed>> $log
|
||||||
|
* @param array<string,string> $status_labels
|
||||||
|
*/
|
||||||
|
private function render_after_sale_log(array $log, array $status_labels): string {
|
||||||
|
if (!$log) {
|
||||||
|
return '<p class="description">' . esc_html__('暂无处理记录', 'aksrt') . '</p>';
|
||||||
|
}
|
||||||
|
$service = \AKSRT\Payment\AfterSaleService::class;
|
||||||
|
$html = '<ul class="aksrt-as-log">';
|
||||||
|
foreach ($log as $entry) {
|
||||||
|
$status = (string) ($entry['status'] ?? '');
|
||||||
|
$label = (string) ($entry['source'] ?? '') === $service::SOURCE_USER
|
||||||
|
? ($status === $service::STATUS_CANCELLED ? esc_html__('用户撤销售后', 'aksrt') : esc_html__('用户提交申请', 'aksrt'))
|
||||||
|
: ($status_labels[$status] ?? $status);
|
||||||
|
$note = (string) ($entry['note'] ?? '');
|
||||||
|
$html .= '<li class="aksrt-as-log__item is-' . esc_attr($status) . '">'
|
||||||
|
. '<span class="aksrt-as-log__time">' . esc_html((string) ($entry['created_at'] ?? '')) . '</span>'
|
||||||
|
. '<span class="aksrt-as-log__status">' . esc_html($label) . '</span>'
|
||||||
|
. '<span class="aksrt-as-log__note">' . ($note !== '' ? nl2br(esc_html($note)) : '—') . '</span>'
|
||||||
|
. '</li>';
|
||||||
|
}
|
||||||
|
return $html . '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle_after_sale_status(): void {
|
||||||
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
||||||
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
||||||
check_admin_referer('aksrt_after_sale_review_' . $id);
|
check_admin_referer('aksrt_after_sale_status_' . $id);
|
||||||
$decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
|
$status = isset($_POST['after_sale_status']) ? sanitize_key((string) wp_unslash($_POST['after_sale_status'])) : '';
|
||||||
$note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
|
$note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
|
||||||
$result = \AKSRT\Payment\AfterSaleService::get_instance()->review($id, $decision, $note);
|
$mark_refund = isset($_POST['mark_order_refunded']) && (string) wp_unslash($_POST['mark_order_refunded']) === '1';
|
||||||
$args = ['tab' => 'after-sales', 'aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_review_failed' : 'after_sale_reviewed'];
|
|
||||||
|
// 勾选「标记订单已退款」时先落订单状态:售后工单一旦离开 pending/processing,
|
||||||
|
// OrderRepository 的退款闸门就不再放行,所以顺序不能反。
|
||||||
|
if ($mark_refund && $this->mark_related_order_refunded($id) === 'failed') {
|
||||||
|
wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, ['aksrt_pay_msg' => 'after_sale_order_refund_failed']));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$result = \AKSRT\Payment\AfterSaleService::get_instance()->set_status($id, $status, $note);
|
||||||
|
$args = ['aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_update_failed' : 'after_sale_updated'];
|
||||||
if (is_wp_error($result)) $args['after_sale_error'] = substr($result->get_error_message(), 0, 240);
|
if (is_wp_error($result)) $args['after_sale_error'] = substr($result->get_error_message(), 0, 240);
|
||||||
wp_safe_redirect($this->page_url(self::PAGE_SLUG, $args)); exit;
|
elseif ($mark_refund) $args['aksrt_pay_msg'] = 'after_sale_updated_order_refunded';
|
||||||
|
wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, $args)); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把售后申请关联的订单推到已退款。仅在该工单仍为待审核/处理中时放行,
|
||||||
|
* 避免对已关闭的工单或非已支付订单误标退款。
|
||||||
|
*
|
||||||
|
* @return string 'done' | 'failed'
|
||||||
|
*/
|
||||||
|
private function mark_related_order_refunded(int $after_sale_id): string {
|
||||||
|
if (!class_exists('\AKSRT\Payment\AfterSaleService') || !class_exists('\AKSRT\Payment\OrderRepository')) return 'failed';
|
||||||
|
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
||||||
|
$row = $service->get($after_sale_id);
|
||||||
|
$order_number = is_array($row) ? (string) ($row['order_number'] ?? '') : '';
|
||||||
|
if ($order_number === '' || !$service->can_finalize_order_refund($after_sale_id, $order_number)) return 'failed';
|
||||||
|
|
||||||
|
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||||
|
$order = $repo->get($order_number);
|
||||||
|
if (is_array($order) && (string) ($order['status'] ?? '') === \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED) return 'done';
|
||||||
|
return $repo->update_status($order_number, \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED, [
|
||||||
|
'allow_refund' => true,
|
||||||
|
'after_sale_id' => $after_sale_id,
|
||||||
|
]) ? 'done' : 'failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_update_order_status(): void {
|
public function handle_update_order_status(): void {
|
||||||
@@ -482,6 +752,47 @@ class PayOrders {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单列表「退款」:把已支付订单推到已退款。退款必须落到一条进行中的售后工单上,
|
||||||
|
* 工单号由服务端按订单号反查,不经客户端传递。
|
||||||
|
*/
|
||||||
|
public function handle_refund_order(): void {
|
||||||
|
if (!current_user_can(self::CAPABILITY)) {
|
||||||
|
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
|
||||||
|
check_admin_referer('aksrt_pay_refund_order_' . $order_number);
|
||||||
|
|
||||||
|
$code = 'order_refund_failed';
|
||||||
|
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
|
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||||
|
$order = $repo->get($order_number);
|
||||||
|
if (!is_array($order)) {
|
||||||
|
$code = 'order_refund_missing';
|
||||||
|
} elseif ((string) ($order['status'] ?? '') !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||||
|
$code = 'order_refund_not_paid';
|
||||||
|
} else {
|
||||||
|
$ticket = class_exists('\AKSRT\Payment\AfterSaleService')
|
||||||
|
? \AKSRT\Payment\AfterSaleService::get_instance()->find_active($order_number)
|
||||||
|
: null;
|
||||||
|
if (!is_array($ticket)) {
|
||||||
|
$code = 'order_refund_no_ticket';
|
||||||
|
} else {
|
||||||
|
$ok = $repo->update_status($order_number, \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED, [
|
||||||
|
'allow_refund' => true,
|
||||||
|
'after_sale_id' => (int) ($ticket['id'] ?? 0),
|
||||||
|
'note' => __('管理员在订单列表中标记退款', 'aksrt'),
|
||||||
|
]);
|
||||||
|
$code = $ok ? 'order_refunded' : 'order_refund_failed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_safe_redirect($this->page_url('aksrt_orders', ['aksrt_pay_msg' => $code]));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
public function handle_update_order_note(): void {
|
public function handle_update_order_note(): void {
|
||||||
if (!current_user_can(self::CAPABILITY)) {
|
if (!current_user_can(self::CAPABILITY)) {
|
||||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||||
@@ -570,8 +881,6 @@ class PayOrders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function ensure_cap(): void {
|
private function ensure_cap(): void {
|
||||||
if (!current_user_can(self::CAPABILITY)) {
|
if (!current_user_can(self::CAPABILITY)) {
|
||||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||||
@@ -622,6 +931,11 @@ class PayOrders {
|
|||||||
$map = [
|
$map = [
|
||||||
'order_updated' => __('订单状态已更新。', 'aksrt'),
|
'order_updated' => __('订单状态已更新。', 'aksrt'),
|
||||||
'order_failed' => __('订单状态更新失败。', 'aksrt'),
|
'order_failed' => __('订单状态更新失败。', 'aksrt'),
|
||||||
|
'order_refunded' => __('订单已标记为已退款。', 'aksrt'),
|
||||||
|
'order_refund_failed' => __('订单标记退款失败。', 'aksrt'),
|
||||||
|
'order_refund_missing' => __('订单不存在。', 'aksrt'),
|
||||||
|
'order_refund_not_paid' => __('仅已支付订单可以标记退款。', 'aksrt'),
|
||||||
|
'order_refund_no_ticket' => __('该订单没有进行中的售后工单,无法标记退款;请先在「售后服务」中处理。', 'aksrt'),
|
||||||
'order_note_updated' => __('订单备注已保存。', 'aksrt'),
|
'order_note_updated' => __('订单备注已保存。', 'aksrt'),
|
||||||
'order_note_failed' => __('订单备注保存失败。', 'aksrt'),
|
'order_note_failed' => __('订单备注保存失败。', 'aksrt'),
|
||||||
'orders_cleared' => sprintf(
|
'orders_cleared' => sprintf(
|
||||||
@@ -638,15 +952,17 @@ class PayOrders {
|
|||||||
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
|
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
|
||||||
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
|
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
|
||||||
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
|
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
|
||||||
'after_sale_reviewed' => __('售后审核已处理。', 'aksrt'),
|
'after_sale_updated' => __('售后状态已更新。', 'aksrt'),
|
||||||
'after_sale_review_failed' => __('售后审核失败,请查看申请状态和错误原因。', 'aksrt'),
|
'after_sale_updated_order_refunded' => __('售后状态已更新,关联订单已标记为已退款。', 'aksrt'),
|
||||||
|
'after_sale_order_refund_failed' => __('关联订单标记已退款失败:需售后工单仍为待审核/处理中,且订单为已支付。', 'aksrt'),
|
||||||
|
'after_sale_update_failed' => __('售后状态更新失败,请查看申请状态和错误原因。', 'aksrt'),
|
||||||
];
|
];
|
||||||
if (!isset($map[$code])) {
|
if (!isset($map[$code])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||||
if ($code === 'after_sale_review_failed' && isset($_GET['after_sale_error'])) {
|
if ($code === 'after_sale_update_failed' && isset($_GET['after_sale_error'])) {
|
||||||
echo '<div class="notice notice-error"><p>' . esc_html(sanitize_text_field((string) wp_unslash($_GET['after_sale_error']))) . '</p></div>';
|
echo '<div class="notice notice-error"><p>' . esc_html(sanitize_text_field((string) wp_unslash($_GET['after_sale_error']))) . '</p></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,27 +970,4 @@ class PayOrders {
|
|||||||
private function page_url(string $slug, array $args = []): string {
|
private function page_url(string $slug, array $args = []): string {
|
||||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stat_card(string $label, string $value, string $url): void {
|
|
||||||
echo '<a href="' . esc_url($url) . '" style="display:block;min-width:140px;padding:16px;background:#fff;border:1px solid #c3c4c7;border-radius:4px;text-decoration:none;color:inherit;">';
|
|
||||||
echo '<div style="font-size:12px;color:#646970;">' . esc_html($label) . '</div>';
|
|
||||||
echo '<div style="font-size:22px;font-weight:600;margin-top:6px;">' . esc_html($value) . '</div>';
|
|
||||||
echo '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ class PayProducts {
|
|||||||
$pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
|
$pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
|
||||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||||
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
||||||
@@ -51,9 +51,22 @@ class PayProducts {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$posts = $query['posts'];
|
$posts = $query['posts'];
|
||||||
$total = $query['total'];
|
$total = (int) $query['total'];
|
||||||
$pages = max(1, (int) ceil($total / $per_page));
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 WP_Query 停在空页上:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$query = $this->query_paid_posts([
|
||||||
|
'pay_type' => $pay_type,
|
||||||
|
'search' => $search,
|
||||||
|
'page' => $page_num,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
]);
|
||||||
|
$posts = $query['posts'];
|
||||||
|
}
|
||||||
|
|
||||||
$sales_map = [];
|
$sales_map = [];
|
||||||
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
|
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
$ids = array_map(static function ($p) {
|
$ids = array_map(static function ($p) {
|
||||||
@@ -146,8 +159,16 @@ class PayProducts {
|
|||||||
echo '</tr>';
|
echo '</tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与 render_filters() 的表单一致。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'pay_type' => $pay_type !== '' ? $pay_type : null,
|
||||||
|
's' => $search !== '' ? $search : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->render_pagination($pages, $page_num);
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,20 +373,4 @@ class PayProducts {
|
|||||||
private function page_url(string $slug, array $args = []): string {
|
private function page_url(string $slug, array $args = []): string {
|
||||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ class PayWithdrawals {
|
|||||||
$date_from = isset($_GET['date_from']) ? $this->sanitize_date((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? $this->sanitize_date((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? $this->sanitize_date((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? $this->sanitize_date((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
||||||
$stats = $svc->get_stats();
|
$stats = $svc->get_stats();
|
||||||
$retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0;
|
$retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0;
|
||||||
@@ -111,6 +111,33 @@ class PayWithdrawals {
|
|||||||
]);
|
]);
|
||||||
$pages = max(1, (int) ceil($total / $per_page));
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$items = $svc->get_all([
|
||||||
|
'status' => $status,
|
||||||
|
'keyword' => $keyword,
|
||||||
|
'user_id' => $user_filter,
|
||||||
|
'date_from' => $date_from,
|
||||||
|
'date_to' => $date_to,
|
||||||
|
'limit' => $per_page,
|
||||||
|
'offset' => ($page_num - 1) * $per_page,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||||
|
// retry_id / retry_key 是一次性动作参数,绝不能进翻页链接。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'status' => $status !== '' ? $status : null,
|
||||||
|
's' => $keyword !== '' ? $keyword : null,
|
||||||
|
'user_id' => $user_filter > 0 ? $user_filter : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped">';
|
echo '<table class="wp-list-table widefat fixed striped">';
|
||||||
echo '<thead><tr>';
|
echo '<thead><tr>';
|
||||||
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||||
@@ -184,18 +211,7 @@ class PayWithdrawals {
|
|||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
|
||||||
if ($pages > 1) {
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
@@ -278,19 +294,4 @@ class PayWithdrawals {
|
|||||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1 ? $date : '';
|
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1 ? $date : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,9 @@ class PostListEnhance {
|
|||||||
|
|
||||||
$order = $this->current_order();
|
$order = $this->current_order();
|
||||||
$order_by = $this->current_orderby();
|
$order_by = $this->current_orderby();
|
||||||
$o_icon = '<i class="opacity5 ml3 fa fa-long-arrow-' . ($order === 'asc' ? 'down' : 'up') . '"></i>';
|
$o_icon = $order === 'asc'
|
||||||
|
? '<i class="opacity5 ml3 fa fas fa-long-arrow-alt-down"></i>'
|
||||||
|
: '<i class="opacity5 ml3 fa fas fa-long-arrow-alt-up"></i>';
|
||||||
|
|
||||||
$new_columns = [];
|
$new_columns = [];
|
||||||
foreach ($columns as $key => $label) {
|
foreach ($columns as $key => $label) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class GlobalSchema {
|
|||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
||||||
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
||||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需先在 WordPress 配置 AI Connectors。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
||||||
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
||||||
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
||||||
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
||||||
@@ -79,12 +79,6 @@ class GlobalSchema {
|
|||||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
|
|
||||||
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
|
|
||||||
]],
|
|
||||||
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
||||||
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ class SchemaExtensionsPush {
|
|||||||
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||||
]],
|
]],
|
||||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
['key' => 'channel', 'title' => '收录通道', 'fields' => [
|
||||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
|
||||||
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
|
|
||||||
]],
|
]],
|
||||||
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
||||||
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
|
||||||
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
|
|||||||
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
||||||
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
||||||
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
||||||
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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', 'sanitize_type' => 'html', 'default' => '立即续费会员,畅享VIP权益', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'help' => '续费会员的一句话简介,支持HTML'],
|
||||||
['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', 'sanitize_type' => 'html', 'default' => '升级VIP会员,享更多会员权益', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '升级会员的一句话简介,支持HTML'],
|
||||||
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", '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', 'sanitize_type' => 'html', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]], 'help' => '显示在开通界面顶部的一句话简介,支持HTML'],
|
||||||
|
['key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>', 'rows' => 4, 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -536,6 +601,19 @@ class SchemaPayment {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private static function vip_product_group_options(): array {
|
||||||
|
return [
|
||||||
|
'collapsible' => true,
|
||||||
|
'defaultCollapsed' => true,
|
||||||
|
'listPreview' => ['prefix' => '价格:', 'titleKey' => 'price', 'descKey' => 'time', 'descUnitKey' => 'unit'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PayPal / Stripe 结算货币选项(参考主题同款 26 种)。
|
* 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', 'sanitize_type' => 'html', 'default' => $level === 1 ? '黄金会员' : '钻石会员', 'dependsOn' => $dep, 'help' => '会员名称,支持HTML'],
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_desc', 'label' => '一句话简介', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '尊享专属特权', 'dependsOn' => $dep, 'help' => '支持HTML'],
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => $equity, 'rows' => 4, 'dependsOn' => $dep, 'help' => '支持安全HTML,建议使用 <li> 列表项'],
|
||||||
|
['key' => 'vip_' . $level . '_product_s', 'label' => '购买会员', 'type' => 'switch', 'default' => true, 'dependsOn' => $dep, 'help' => '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能'],
|
||||||
|
[
|
||||||
|
'key' => 'vip_' . $level . '_product', 'label' => '会员商品', 'type' => 'group', 'max' => 8,
|
||||||
|
'button_title' => '添加会员商品', 'default' => self::vip_product_defaults(),
|
||||||
|
...self::vip_product_group_options(),
|
||||||
|
'dependsOn' => ['all' => [$dep, ['key' => 'vip_' . $level . '_product_s', 'value' => true]]],
|
||||||
|
'help' => '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||||
|
'fields' => self::vip_product_fields(),
|
||||||
|
],
|
||||||
|
['key' => 'vip_' . $level . '_img_icon', 'label' => '会员图标', 'type' => 'upload', 'default' => '', 'dependsOn' => $dep, 'help' => '留空使用内置 vip-' . $level . '.svg 图标;建议尺寸300x300'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private static function vip_product_defaults(): array {
|
||||||
|
return [
|
||||||
|
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||||
|
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签颜色调色板(参考主题 zib_palette(array(), array('b','jb')))。
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,string>>
|
||||||
|
*/
|
||||||
|
private static function vip_tag_palette(): array {
|
||||||
|
$colors = [
|
||||||
|
'b-red' => '#f74b3d',
|
||||||
|
'b-yellow' => '#f3920a',
|
||||||
|
'b-cyan' => '#08c4c1',
|
||||||
|
'b-blue' => '#0a8cf3',
|
||||||
|
'b-green' => '#1fd05a',
|
||||||
|
'b-purple' => '#c133f5',
|
||||||
|
'b-black' => '#121517',
|
||||||
|
'jb-red' => 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)',
|
||||||
|
'jb-pink' => 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)',
|
||||||
|
'jb-yellow' => 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)',
|
||||||
|
'jb-cyan' => 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)',
|
||||||
|
'jb-blue' => 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)',
|
||||||
|
'jb-green' => 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)',
|
||||||
|
'jb-purple' => 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)',
|
||||||
|
'jb-vip1' => 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)',
|
||||||
|
'jb-vip2' => 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)',
|
||||||
|
];
|
||||||
|
$options = [];
|
||||||
|
foreach ($colors as $value => $css) {
|
||||||
|
$options[] = ['label' => $css, 'value' => $value];
|
||||||
|
}
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,8 @@ class SchemaPermissions {
|
|||||||
['label' => '每日签到', 'value' => 'signin'],
|
['label' => '每日签到', 'value' => 'signin'],
|
||||||
['label' => '前台投稿', 'value' => 'front_submit'],
|
['label' => '前台投稿', 'value' => 'front_submit'],
|
||||||
['label' => '打赏用户', 'value' => 'tip_user'],
|
['label' => '打赏用户', 'value' => 'tip_user'],
|
||||||
|
['label' => '申请售后', 'value' => 'after_sale_apply'],
|
||||||
|
['label' => '撤销售后', 'value' => 'after_sale_cancel'],
|
||||||
]],
|
]],
|
||||||
]],
|
]],
|
||||||
['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],
|
['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
+9
-314
@@ -11,9 +11,6 @@ class AI {
|
|||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
const PROVIDER_NONE = 'none';
|
|
||||||
const PROVIDER_OPENAI = 'openai';
|
|
||||||
|
|
||||||
public static function get_instance(): self {
|
public static function get_instance(): self {
|
||||||
if (null === self::$instance) {
|
if (null === self::$instance) {
|
||||||
self::$instance = new self();
|
self::$instance = new self();
|
||||||
@@ -21,95 +18,10 @@ class AI {
|
|||||||
return self::$instance;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 AI 配置(主题侧 provider)
|
|
||||||
*/
|
|
||||||
public function get_config(): array {
|
|
||||||
$defaults = [
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => self::PROVIDER_NONE,
|
|
||||||
'api_key' => '',
|
|
||||||
'api_base' => 'https://api.openai.com/v1',
|
|
||||||
'model' => 'gpt-3.5-turbo',
|
|
||||||
'max_tokens' => 1000,
|
|
||||||
'rate_limit' => 10,
|
|
||||||
];
|
|
||||||
|
|
||||||
$saved = get_option('aksrt_ai_settings', []);
|
|
||||||
if (!is_array($saved)) {
|
|
||||||
$saved = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_merge($defaults, $saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 AI 配置
|
|
||||||
*/
|
|
||||||
public function update_config(array $config): bool {
|
|
||||||
$current = $this->get_config();
|
|
||||||
$merged = array_merge($current, $config);
|
|
||||||
|
|
||||||
// Preserve existing key when client sends the masked placeholder.
|
|
||||||
if (isset($config['api_key']) && $config['api_key'] === '***') {
|
|
||||||
$merged['api_key'] = $current['api_key'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$merged['enabled'] = !empty($merged['enabled']);
|
|
||||||
$merged['provider'] = sanitize_key((string) ($merged['provider'] ?? self::PROVIDER_NONE));
|
|
||||||
$merged['api_key'] = (string) ($merged['api_key'] ?? '');
|
|
||||||
$merged['api_base'] = $this->normalize_api_base((string) ($merged['api_base'] ?? 'https://api.openai.com/v1'));
|
|
||||||
$merged['model'] = sanitize_text_field((string) ($merged['model'] ?? 'gpt-3.5-turbo'));
|
|
||||||
$merged['max_tokens'] = max(100, min(4000, (int) ($merged['max_tokens'] ?? 1000)));
|
|
||||||
$merged['rate_limit'] = max(1, min(100, (int) ($merged['rate_limit'] ?? 10)));
|
|
||||||
|
|
||||||
// Persist only the known keys so shape stays stable.
|
|
||||||
$to_store = [
|
|
||||||
'enabled' => $merged['enabled'],
|
|
||||||
'provider' => $merged['provider'],
|
|
||||||
'api_key' => $merged['api_key'],
|
|
||||||
'api_base' => $merged['api_base'],
|
|
||||||
'model' => $merged['model'],
|
|
||||||
'max_tokens' => $merged['max_tokens'],
|
|
||||||
'rate_limit' => $merged['rate_limit'],
|
|
||||||
];
|
|
||||||
|
|
||||||
$before = get_option('aksrt_ai_settings', []);
|
|
||||||
$updated = update_option('aksrt_ai_settings', $to_store);
|
|
||||||
if ($updated) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update_option returns false when the value is unchanged — treat as success.
|
|
||||||
if (is_array($before)) {
|
|
||||||
$before_normalized = array_merge([
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => self::PROVIDER_NONE,
|
|
||||||
'api_key' => '',
|
|
||||||
'api_base' => 'https://api.openai.com/v1',
|
|
||||||
'model' => 'gpt-3.5-turbo',
|
|
||||||
'max_tokens' => 1000,
|
|
||||||
'rate_limit' => 10,
|
|
||||||
], $before);
|
|
||||||
$before_normalized['enabled'] = !empty($before_normalized['enabled']);
|
|
||||||
$before_normalized['provider'] = sanitize_key((string) ($before_normalized['provider'] ?? self::PROVIDER_NONE));
|
|
||||||
$before_normalized['api_key'] = (string) ($before_normalized['api_key'] ?? '');
|
|
||||||
$before_normalized['api_base'] = esc_url_raw((string) ($before_normalized['api_base'] ?? 'https://api.openai.com/v1'));
|
|
||||||
$before_normalized['model'] = sanitize_text_field((string) ($before_normalized['model'] ?? 'gpt-3.5-turbo'));
|
|
||||||
$before_normalized['max_tokens'] = max(100, min(4000, (int) ($before_normalized['max_tokens'] ?? 1000)));
|
|
||||||
$before_normalized['rate_limit'] = max(1, min(100, (int) ($before_normalized['rate_limit'] ?? 10)));
|
|
||||||
|
|
||||||
return maybe_serialize($before_normalized) === maybe_serialize($to_store)
|
|
||||||
|| $before_normalized == $to_store;
|
|
||||||
}
|
|
||||||
|
|
||||||
return maybe_serialize($before) === maybe_serialize($to_store);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WordPress 核心 AI Client 是否真正可用。
|
* WordPress 核心 AI Client 是否真正可用。
|
||||||
*
|
*
|
||||||
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应抢占主题 provider。
|
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应声称可用。
|
||||||
*/
|
*/
|
||||||
public function is_wp_ai_available(): bool {
|
public function is_wp_ai_available(): bool {
|
||||||
if (!function_exists('wp_ai_client_prompt')) {
|
if (!function_exists('wp_ai_client_prompt')) {
|
||||||
@@ -124,89 +36,24 @@ class AI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题 OpenAI 兼容 provider 是否可用。
|
* AI 生成是否可用(连接由 WordPress AI Connectors 提供)。
|
||||||
*/
|
|
||||||
public function is_theme_provider_available(): bool {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$provider = sanitize_key((string) ($config['provider'] ?? self::PROVIDER_NONE));
|
|
||||||
$api_key = trim((string) ($config['api_key'] ?? ''));
|
|
||||||
$api_base = trim((string) ($config['api_base'] ?? ''));
|
|
||||||
|
|
||||||
return !empty($config['enabled'])
|
|
||||||
&& $provider !== self::PROVIDER_NONE
|
|
||||||
&& $provider !== ''
|
|
||||||
&& $api_key !== ''
|
|
||||||
&& $api_base !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任一后端可用即可生成。
|
|
||||||
*/
|
*/
|
||||||
public function is_available(): bool {
|
public function is_available(): bool {
|
||||||
return $this->is_theme_provider_available() || $this->is_wp_ai_available();
|
return $this->is_wp_ai_available();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前优先后端标识。
|
|
||||||
*
|
|
||||||
* Theme provider wins when explicitly configured — WP AI stubs often exist
|
|
||||||
* without a working connector and previously produced opaque 400 failures.
|
|
||||||
*/
|
|
||||||
public function resolve_backend(): string {
|
|
||||||
if ($this->is_theme_provider_available()) {
|
|
||||||
return 'theme';
|
|
||||||
}
|
|
||||||
if ($this->is_wp_ai_available()) {
|
|
||||||
return 'wp_ai';
|
|
||||||
}
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 AI 状态(不回显完整密钥)。
|
|
||||||
*/
|
|
||||||
public function get_status(): array {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
|
||||||
|
|
||||||
return [
|
|
||||||
'enabled' => !empty($config['enabled']),
|
|
||||||
'provider' => (string) ($config['provider'] ?? self::PROVIDER_NONE),
|
|
||||||
'configured' => !empty($config['api_key']) || $this->is_wp_ai_available(),
|
|
||||||
'model' => (string) ($config['model'] ?? ''),
|
|
||||||
'api_base' => (string) ($config['api_base'] ?? ''),
|
|
||||||
'api_key' => !empty($config['api_key']) ? '***' : '',
|
|
||||||
'max_tokens' => (int) ($config['max_tokens'] ?? 1000),
|
|
||||||
'rate_limit' => (int) ($config['rate_limit'] ?? 10),
|
|
||||||
'available' => $this->is_available(),
|
|
||||||
'backend' => $this->resolve_backend(),
|
|
||||||
'wp_ai_available' => $this->is_wp_ai_available(),
|
|
||||||
'theme_available' => $this->is_theme_provider_available(),
|
|
||||||
'seo_enabled' => !empty($seo['seo_enabled']),
|
|
||||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']),
|
|
||||||
'write_assist' => $this->is_available(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一文本生成出口。
|
* 统一文本生成出口。
|
||||||
*
|
*
|
||||||
* @param string $prompt 用户提示 / 上下文
|
* @param string $prompt 用户提示 / 上下文
|
||||||
* @param array $options system_instruction, max_tokens, temperature
|
* @param array $options system_instruction(其余选项由 WP AI Client 忽略)
|
||||||
* @return array{success:bool,content?:string,message?:string,backend?:string}
|
* @return array{success:bool,content?:string,message?:string,backend?:string}
|
||||||
*/
|
*/
|
||||||
public function generate_text(string $prompt, array $options = []): array {
|
public function generate_text(string $prompt, array $options = []): array {
|
||||||
if (!$this->is_available()) {
|
if (!$this->is_available()) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
'message' => __('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->check_rate_limit()) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => __('请求过于频繁,请稍后再试', 'aksrt'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,42 +61,12 @@ class AI {
|
|||||||
? (string) $options['system_instruction']
|
? (string) $options['system_instruction']
|
||||||
: __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt');
|
: __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt');
|
||||||
|
|
||||||
$last_error = [
|
$result = $this->generate_via_wp_ai($prompt, $system, $options);
|
||||||
'success' => false,
|
if (!empty($result['success'])) {
|
||||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
$result['backend'] = 'wp_ai';
|
||||||
];
|
|
||||||
|
|
||||||
// 1) Prefer explicitly configured theme OpenAI-compatible provider.
|
|
||||||
if ($this->is_theme_provider_available()) {
|
|
||||||
$theme_result = $this->generate_via_theme($prompt, $system, $options);
|
|
||||||
$this->record_call();
|
|
||||||
if (!empty($theme_result['success'])) {
|
|
||||||
$theme_result['backend'] = 'theme';
|
|
||||||
return $theme_result;
|
|
||||||
}
|
|
||||||
$last_error = $theme_result;
|
|
||||||
// Fall through to WP AI if available.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) WordPress AI Client (Connectors).
|
return $result;
|
||||||
if ($this->is_wp_ai_available()) {
|
|
||||||
$wp_result = $this->generate_via_wp_ai($prompt, $system, $options);
|
|
||||||
$this->record_call();
|
|
||||||
if (!empty($wp_result['success'])) {
|
|
||||||
$wp_result['backend'] = 'wp_ai';
|
|
||||||
return $wp_result;
|
|
||||||
}
|
|
||||||
$last_error = $wp_result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $last_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 兼容旧调用。
|
|
||||||
*/
|
|
||||||
public function generate(string $prompt, array $options = []): array {
|
|
||||||
return $this->generate_text($prompt, $options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generate_via_wp_ai(string $prompt, string $system, array $options): array {
|
private function generate_via_wp_ai(string $prompt, string $system, array $options): array {
|
||||||
@@ -289,126 +106,4 @@ class AI {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize OpenAI-compatible base URL.
|
|
||||||
*
|
|
||||||
* Accepts gateway root (https://host), /v1, trailing slash, or full
|
|
||||||
* .../chat/completions and returns a base that can append /chat/completions.
|
|
||||||
*/
|
|
||||||
public function normalize_api_base(string $api_base): string {
|
|
||||||
$api_base = trim($api_base);
|
|
||||||
if ($api_base === '') {
|
|
||||||
return 'https://api.openai.com/v1';
|
|
||||||
}
|
|
||||||
|
|
||||||
$api_base = esc_url_raw($api_base);
|
|
||||||
$api_base = rtrim($api_base, '/');
|
|
||||||
|
|
||||||
// If user pasted the full chat endpoint, strip the leaf.
|
|
||||||
if (preg_match('#/chat/completions$#i', $api_base)) {
|
|
||||||
$api_base = (string) preg_replace('#/chat/completions$#i', '', $api_base);
|
|
||||||
$api_base = rtrim($api_base, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common gateways (NewAPI etc.): root host needs /v1.
|
|
||||||
if (!preg_match('#/(v1|openai/v1)$#i', $api_base)) {
|
|
||||||
$api_base .= '/v1';
|
|
||||||
}
|
|
||||||
|
|
||||||
return rtrim($api_base, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generate_via_theme(string $prompt, string $system, array $options): array {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$max_tokens = (int) ($options['max_tokens'] ?? $config['max_tokens']);
|
|
||||||
$temperature = (float) ($options['temperature'] ?? 0.7);
|
|
||||||
$api_base = $this->normalize_api_base((string) $config['api_base']);
|
|
||||||
|
|
||||||
$response = wp_remote_post($api_base . '/chat/completions', [
|
|
||||||
'headers' => [
|
|
||||||
'Content-Type' => 'application/json',
|
|
||||||
'Authorization' => 'Bearer ' . $config['api_key'],
|
|
||||||
],
|
|
||||||
'body' => wp_json_encode([
|
|
||||||
'model' => $config['model'],
|
|
||||||
'messages' => [
|
|
||||||
['role' => 'system', 'content' => $system],
|
|
||||||
['role' => 'user', 'content' => $prompt],
|
|
||||||
],
|
|
||||||
'max_tokens' => $max_tokens,
|
|
||||||
'temperature' => $temperature,
|
|
||||||
]),
|
|
||||||
'timeout' => 45,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => sprintf(
|
|
||||||
/* translators: %s: error message */
|
|
||||||
__('API 请求失败: %s', 'aksrt'),
|
|
||||||
$response->get_error_message()
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
|
||||||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
|
||||||
|
|
||||||
if ($status_code !== 200) {
|
|
||||||
$error_msg = is_array($body) ? ($body['error']['message'] ?? __('未知错误', 'aksrt')) : __('未知错误', 'aksrt');
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => sprintf(
|
|
||||||
/* translators: 1: HTTP status, 2: error message */
|
|
||||||
__('API 错误 (%1$d): %2$s', 'aksrt'),
|
|
||||||
$status_code,
|
|
||||||
$error_msg
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = is_array($body) ? ($body['choices'][0]['message'] ?? []) : [];
|
|
||||||
$content = is_array($message) ? (string) ($message['content'] ?? '') : '';
|
|
||||||
|
|
||||||
if ($content === '' && is_array($message) && !empty($message['reasoning_content'])) {
|
|
||||||
$content = (string) $message['reasoning_content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = trim($content);
|
|
||||||
if ($content === '') {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => __('AI 返回内容为空(可能 max_tokens 不足)', 'aksrt'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'content' => $content,
|
|
||||||
'reasoning' => is_array($message) ? (string) ($message['reasoning_content'] ?? '') : '',
|
|
||||||
'usage' => is_array($body) ? ($body['usage'] ?? []) : [],
|
|
||||||
'finish_reason' => is_array($body) ? (string) ($body['choices'][0]['finish_reason'] ?? '') : '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function check_rate_limit(): bool {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$limit = (int) $config['rate_limit'];
|
|
||||||
if ($limit <= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
|
||||||
$count = (int) get_transient($key);
|
|
||||||
|
|
||||||
return $count < $limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function record_call(): void {
|
|
||||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
|
||||||
$count = (int) get_transient($key);
|
|
||||||
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -77,31 +77,6 @@ class AIPrompt {
|
|||||||
6. 直接输出描述文本本身,不要任何前缀、引号或解释。';
|
6. 直接输出描述文本本身,不要任何前缀、引号或解释。';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 兼容旧单 prompt 构建(草稿/无 ID 场景)。
|
|
||||||
*/
|
|
||||||
public function build_seo_prompt(string $title, string $content): string {
|
|
||||||
$title_instruction = $this->get_seo_title_system_instruction();
|
|
||||||
$kw_instruction = $this->get_seo_keywords_system_instruction();
|
|
||||||
$desc_instruction = $this->get_seo_description_system_instruction();
|
|
||||||
|
|
||||||
return <<<PROMPT
|
|
||||||
请根据以下文章内容,生成 SEO 优化建议。
|
|
||||||
|
|
||||||
文章标题:{$title}
|
|
||||||
|
|
||||||
文章内容:
|
|
||||||
{$content}
|
|
||||||
|
|
||||||
请以 JSON 格式返回,包含以下字段:
|
|
||||||
- suggested_title: {$title_instruction}
|
|
||||||
- suggested_description: {$desc_instruction}
|
|
||||||
- suggested_keywords: {$kw_instruction}(以 JSON 数组形式返回)
|
|
||||||
|
|
||||||
只返回 JSON,不要其他内容。
|
|
||||||
PROMPT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function build_summary_prompt(string $content, int $max_length = 200): string {
|
public function build_summary_prompt(string $content, int $max_length = 200): string {
|
||||||
$content = mb_substr($content, 0, 3000);
|
$content = mb_substr($content, 0, 3000);
|
||||||
return <<<PROMPT
|
return <<<PROMPT
|
||||||
|
|||||||
@@ -107,67 +107,6 @@ class AISEO {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 草稿场景:用传入 title/content 生成(兼容旧 /ai/seo-suggest)。
|
|
||||||
*/
|
|
||||||
public function suggest(string $title, string $content = ''): array {
|
|
||||||
if (!$this->is_enabled()) {
|
|
||||||
return ['success' => false, 'message' => __('AI SEO 功能未启用', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists('AKSRT\\Ai\\AI') || !class_exists('AKSRT\\Ai\\AIPrompt')) {
|
|
||||||
return ['success' => false, 'message' => __('AI 模块未加载', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ai = AI::get_instance();
|
|
||||||
if (!$ai->is_available()) {
|
|
||||||
return ['success' => false, 'message' => __('AI 服务未启用', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = trim($title);
|
|
||||||
$content = trim(wp_strip_all_tags($content));
|
|
||||||
if ($title === '' && $content === '') {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => __('无可用内容,请先填写标题或正文', 'aksrt'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = [];
|
|
||||||
if ($title !== '') {
|
|
||||||
$parts[] = '标题: ' . $title;
|
|
||||||
}
|
|
||||||
if ($content !== '') {
|
|
||||||
$content = wp_trim_words($content, 1500, '');
|
|
||||||
$parts[] = "正文:\n" . $content;
|
|
||||||
}
|
|
||||||
$context = implode("\n\n", $parts);
|
|
||||||
$prompt = AIPrompt::get_instance();
|
|
||||||
|
|
||||||
$title_out = $this->run_field($ai, $context, $prompt->get_seo_title_system_instruction(), 'title');
|
|
||||||
$kw_out = $this->run_field($ai, $context, $prompt->get_seo_keywords_system_instruction(), 'keywords');
|
|
||||||
$desc_out = $this->run_field($ai, $context, $prompt->get_seo_description_system_instruction(), 'description');
|
|
||||||
|
|
||||||
$keywords = [];
|
|
||||||
if (!empty($kw_out['content'])) {
|
|
||||||
$keywords = array_values(array_filter(array_map('trim', explode(',', $kw_out['content']))));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'suggested_title' => (string) ($title_out['content'] ?? ''),
|
|
||||||
'suggested_description' => (string) ($desc_out['content'] ?? ''),
|
|
||||||
'suggested_keywords' => $keywords,
|
|
||||||
'fields' => [
|
|
||||||
'title' => $title_out,
|
|
||||||
'keywords' => $kw_out,
|
|
||||||
'description' => $desc_out,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string|\WP_Error
|
* @return string|\WP_Error
|
||||||
*/
|
*/
|
||||||
|
|||||||
+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 {
|
||||||
|
|||||||
@@ -38,8 +38,9 @@ trait VipServiceModal {
|
|||||||
: wp_kses_post($equity);
|
: wp_kses_post($equity);
|
||||||
$level_view[] = [
|
$level_view[] = [
|
||||||
'level' => $level,
|
'level' => $level,
|
||||||
'name' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
'name' => wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level))),
|
||||||
'desc' => (string) ($levels[$level]['desc'] ?? ''),
|
'name_text' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
||||||
|
'desc' => wp_kses_post((string) ($levels[$level]['desc'] ?? '')),
|
||||||
'equity' => $equity,
|
'equity' => $equity,
|
||||||
'img_icon' => esc_url_raw((string) ($levels[$level]['img_icon'] ?? '')),
|
'img_icon' => esc_url_raw((string) ($levels[$level]['img_icon'] ?? '')),
|
||||||
];
|
];
|
||||||
@@ -115,7 +116,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,15 +215,17 @@ 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)
|
||||||
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
? esc_html__('永久', 'aksrt')
|
||||||
|
: (string) $pkg_time . '<span class="px12">' . esc_html($pkg_unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')) . '</span>';
|
||||||
|
$level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||||
|
|
||||||
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
||||||
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . esc_html($level_name) . '</span>'
|
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . wp_kses_post($level_name) . '</span>'
|
||||||
. '<div class="flex jsb ac hh em16 mt10">'
|
. '<div class="flex jsb ac hh em16 mt10">'
|
||||||
. '<span class="shrink0 mr10">' . $duration . '</span>'
|
. '<span class="shrink0 mr10">' . $duration . '</span>'
|
||||||
. '<span class="shrink0">' . $points . '<span class="px12">' . esc_html($currency) . '</span></span>'
|
. '<span class="shrink0">' . $points . '<span class="px12">' . esc_html($currency) . '</span></span>'
|
||||||
@@ -237,16 +239,18 @@ trait VipServiceModal {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$desc_text = trim((string) ($points_ex['desc'] ?? ''));
|
$desc_text = trim((string) ($vip['points_exchange']['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 aksrt-vip-points-desc">' . wp_kses_post($desc_text) . '</div>'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
||||||
// (参考主题源码写作 `!$min_val > $user_points`,因运算符优先级恒假,
|
// (参考主题源码写作 `!$min_val > $user_points`,因运算符优先级恒假,
|
||||||
// 此处按其原意取真实比较,保证提示分支可达。)
|
// 此处按其原意取真实比较,保证提示分支可达。)
|
||||||
if ($min_points > $user_points) {
|
if ($min_points > $user_points) {
|
||||||
$form = '<div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div>';
|
$form = '<div class="mt20"><div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div></div>';
|
||||||
} else {
|
} else {
|
||||||
$form = '<div class="mt20">'
|
$form = '<div class="mt20">'
|
||||||
. '<input type="hidden" name="product_id" value="' . $first_package_id . '">'
|
. '<input type="hidden" name="product_id" value="' . $first_package_id . '">'
|
||||||
@@ -289,14 +293,14 @@ trait VipServiceModal {
|
|||||||
* @param array<string,mixed> $level_settings 等级设置(含 name / img_icon)。
|
* @param array<string,mixed> $level_settings 等级设置(含 name / img_icon)。
|
||||||
*/
|
*/
|
||||||
private function points_exchange_card_icon(array $level_settings): string {
|
private function points_exchange_card_icon(array $level_settings): string {
|
||||||
$name = (string) ($level_settings['name'] ?? '');
|
$name_text = (string) ($level_settings['name'] ?? '');
|
||||||
$icon_url = (string) ($level_settings['img_icon'] ?? '');
|
$icon_url = (string) ($level_settings['img_icon'] ?? '');
|
||||||
if ($icon_url === '') {
|
if ($icon_url === '') {
|
||||||
return '<i class="fa fas fa-certificate mr3" aria-hidden="true"></i>';
|
return '<i class="fa fas fa-certificate mr3" aria-hidden="true"></i>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon mr3')
|
return '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon mr3')
|
||||||
. ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '">';
|
. ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '">';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,26 +331,27 @@ trait VipServiceModal {
|
|||||||
if (empty($levels[$level]['enabled'])) {
|
if (empty($levels[$level]['enabled'])) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
$name = wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level)));
|
||||||
|
$name_text = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||||
$equity = (string) ($levels[$level]['equity'] ?? '');
|
$equity = (string) ($levels[$level]['equity'] ?? '');
|
||||||
$cls = $user_id > 0 ? ' pay-vip' : ' signin-loader';
|
$cls = $user_id > 0 ? ' pay-vip' : ' signin-loader';
|
||||||
|
|
||||||
$equity_html = $equity !== ''
|
$equity_html = $equity !== '' && function_exists('aksrt_format_vip_equity_html')
|
||||||
? '<ul class="mb10 relative">' . wp_kses_post($equity) . '</ul>'
|
? aksrt_format_vip_equity_html($equity, 'mb10 relative')
|
||||||
: '';
|
: ($equity !== '' ? '<div class="v-equity mb10 relative">' . wp_kses_post($equity) . '</div>' : '');
|
||||||
|
|
||||||
$icon_url = esc_url((string) ($levels[$level]['img_icon'] ?? ''));
|
$icon_url = esc_url((string) ($levels[$level]['img_icon'] ?? ''));
|
||||||
$icon = $icon_url !== ''
|
$icon = $icon_url !== ''
|
||||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '" decoding="async">'
|
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '" decoding="async">'
|
||||||
: '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
: '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
||||||
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . $cls . '" vip-level="' . $level . '">';
|
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . $cls . '" vip-level="' . $level . '">';
|
||||||
$html .= '<div class="abs-center vip-baicon" aria-hidden="true">' . $icon . '</div>';
|
$html .= '<div class="abs-center vip-baicon" aria-hidden="true">' . $icon . '</div>';
|
||||||
$html .= '<div class="vip-img abs-right">' . $icon . '</div>';
|
$html .= '<div class="vip-img abs-right">' . $icon . '</div>';
|
||||||
$html .= '<div class="relative">';
|
$html .= '<div class="relative">';
|
||||||
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . esc_html($name) . '</div>';
|
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . wp_kses_post($name) . '</div>';
|
||||||
$html .= $equity_html;
|
$html .= $equity_html;
|
||||||
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
|
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
|
||||||
. esc_html(sprintf(__('开通%s', 'aksrt'), $name))
|
. wp_kses_post(sprintf(__('开通%s', 'aksrt'), $name))
|
||||||
. '<span class="ml6"><i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></span></a>';
|
. '<span class="ml6"><i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></span></a>';
|
||||||
$html .= '</div></div>';
|
$html .= '</div></div>';
|
||||||
return $html;
|
return $html;
|
||||||
|
|||||||
+120
-314
@@ -11,82 +11,6 @@ trait VipServicePackages {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse per-level product lines (legacy vip_product).
|
|
||||||
*
|
|
||||||
* Preferred: price|show_price|time|unit|tag|title
|
|
||||||
* Legacy: price|days|title
|
|
||||||
*
|
|
||||||
* @return array<int, array{price:string,show_price:string,time:int,unit:string,tag:string,tag_class:string,title:string,days:int}>
|
|
||||||
*/
|
|
||||||
public function parse_level_products(string $raw): array {
|
|
||||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
|
||||||
$out = [];
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$line = trim((string) $line);
|
|
||||||
if ($line === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$parts = array_map('trim', explode('|', $line));
|
|
||||||
$price_minor = $this->parse_vip_money_minor($parts[0] ?? '');
|
|
||||||
if ($price_minor <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$n = count($parts);
|
|
||||||
// New format: price|show_price|time|unit|tag|title (6+)
|
|
||||||
// Or: price|show_price|time|unit|title (5)
|
|
||||||
if ($n >= 4 && (in_array(strtolower((string) ($parts[3] ?? '')), ['day', 'month', 'd', 'm'], true)
|
|
||||||
|| (isset($parts[3]) && $parts[3] !== '' && !is_numeric($parts[3])))) {
|
|
||||||
$show_minor = $this->parse_vip_money_minor($parts[1] ?? '0');
|
|
||||||
$time = isset($parts[2]) ? (int) $parts[2] : 0;
|
|
||||||
$unit_raw = strtolower((string) ($parts[3] ?? 'month'));
|
|
||||||
$unit = ($unit_raw === 'day' || $unit_raw === 'd') ? 'day' : 'month';
|
|
||||||
$tag = '';
|
|
||||||
$title = '';
|
|
||||||
if ($n >= 6) {
|
|
||||||
$tag = sanitize_text_field((string) ($parts[4] ?? ''));
|
|
||||||
$title = sanitize_text_field((string) ($parts[5] ?? ''));
|
|
||||||
} elseif ($n === 5) {
|
|
||||||
// price|show|time|unit|title
|
|
||||||
$title = sanitize_text_field((string) ($parts[4] ?? ''));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Legacy: price|days|title
|
|
||||||
$show_minor = 0;
|
|
||||||
$days_legacy = isset($parts[1]) ? (int) $parts[1] : 0;
|
|
||||||
$time = $days_legacy;
|
|
||||||
$unit = 'day';
|
|
||||||
$tag = '';
|
|
||||||
$title = isset($parts[2]) ? sanitize_text_field($parts[2]) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$time = max(0, $time);
|
|
||||||
$days = $this->duration_to_days($time, $unit);
|
|
||||||
if ($title === '') {
|
|
||||||
if ($time <= 0) {
|
|
||||||
$title = __('永久会员', 'aksrt');
|
|
||||||
} elseif ($unit === 'day') {
|
|
||||||
$title = sprintf(__('%d天会员', 'aksrt'), $time);
|
|
||||||
} else {
|
|
||||||
$title = sprintf(__('%d个月会员', 'aksrt'), $time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$out[] = [
|
|
||||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
|
||||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor),
|
|
||||||
'time' => $time,
|
|
||||||
'unit' => $unit,
|
|
||||||
'tag' => $tag,
|
|
||||||
'tag_class' => '',
|
|
||||||
'title' => $title,
|
|
||||||
'days' => $days,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Approximate days for catalog labels / order meta; activate prefers time+unit calendar math.
|
* 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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,22 +137,32 @@ trait VipServicePurchase {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
$name = trim(wp_strip_all_tags((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'] ?? ''),
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ class DBMigrator {
|
|||||||
// 2.5.1: order_aftersale 新增 images 列(售后举证图片)。dbDelta 仅在版本变化时
|
// 2.5.1: order_aftersale 新增 images 列(售后举证图片)。dbDelta 仅在版本变化时
|
||||||
// 重跑,新增/调整列必须同步升版本,否则老安装不会执行 ALTER TABLE。
|
// 重跑,新增/调整列必须同步升版本,否则老安装不会执行 ALTER TABLE。
|
||||||
// 2.5.2: order_aftersale 新增 apply_form 列(结构化申请表单:预置原因 / 申请资料 / 申请说明)。
|
// 2.5.2: order_aftersale 新增 apply_form 列(结构化申请表单:预置原因 / 申请资料 / 申请说明)。
|
||||||
const SCHEMA_VERSION = '2.5.2';
|
// 2.6.0: order_aftersale 新增 handling_log 列(处理记录:每次状态提交一条
|
||||||
|
// 「状态 + 备注 + 来源 + 时间」),取代单一的 admin_note / reviewed_at / completed_at,
|
||||||
|
// 并移除从未被写入的 gateway_request_id / gateway_refund_id / gateway_result。
|
||||||
|
const SCHEMA_VERSION = '2.6.0';
|
||||||
|
|
||||||
const OPT_KEY = 'aksrt_db_schema_version';
|
const OPT_KEY = 'aksrt_db_schema_version';
|
||||||
|
|
||||||
|
|||||||
@@ -213,15 +213,10 @@ trait DBMigratorPart2 {
|
|||||||
reason TEXT NOT NULL,
|
reason TEXT NOT NULL,
|
||||||
apply_form LONGTEXT DEFAULT NULL,
|
apply_form LONGTEXT DEFAULT NULL,
|
||||||
images LONGTEXT DEFAULT NULL,
|
images LONGTEXT DEFAULT NULL,
|
||||||
|
handling_log LONGTEXT DEFAULT NULL,
|
||||||
refund_amount VARCHAR(32) NOT NULL DEFAULT '0',
|
refund_amount VARCHAR(32) NOT NULL DEFAULT '0',
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
admin_note TEXT DEFAULT NULL,
|
|
||||||
gateway_request_id VARCHAR(128) DEFAULT NULL,
|
|
||||||
gateway_refund_id VARCHAR(128) DEFAULT NULL,
|
|
||||||
gateway_result LONGTEXT DEFAULT NULL,
|
|
||||||
created_at DATETIME NOT NULL,
|
created_at DATETIME NOT NULL,
|
||||||
reviewed_at DATETIME DEFAULT NULL,
|
|
||||||
completed_at DATETIME DEFAULT NULL,
|
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY idx_order_status (order_number, status),
|
KEY idx_order_status (order_number, status),
|
||||||
KEY idx_user_created (user_id, created_at),
|
KEY idx_user_created (user_id, created_at),
|
||||||
|
|||||||
@@ -210,16 +210,16 @@ class SEOMetaBox {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
$status = class_exists('\AKSRT\Ai\AI') ? \AKSRT\Ai\AI::get_instance()->get_status() : [];
|
$ai_available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||||
|
|
||||||
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
||||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||||
'nonce' => wp_create_nonce('wp_rest'),
|
'nonce' => wp_create_nonce('wp_rest'),
|
||||||
'ai' => [
|
'ai' => [
|
||||||
'available' => !empty($status['available']),
|
'available' => $ai_available,
|
||||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
||||||
'write_assist' => !empty($status['available']),
|
'write_assist' => $ai_available,
|
||||||
],
|
],
|
||||||
'i18n' => [
|
'i18n' => [
|
||||||
'generating' => __('生成中…', 'aksrt'),
|
'generating' => __('生成中…', 'aksrt'),
|
||||||
@@ -262,7 +262,7 @@ class SEOMetaBox {
|
|||||||
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||||
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
||||||
$title = !$available
|
$title = !$available
|
||||||
? esc_attr__('请先配置 AI 连接(扩展 → AI 连接配置 或 WP Connectors)', 'aksrt')
|
? esc_attr__('请先配置 WordPress AI Connectors', 'aksrt')
|
||||||
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -314,8 +314,13 @@ class Signin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$checkin_all_day = $total ? '<div class="mb6 em09">' . sprintf(__('累计签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $total) . '</badge>') . '</div>' : '';
|
$checkin_all_day = $total ? '<div class="mb6 em09">' . sprintf(__('累计签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $total) . '</badge>') . '</div>' : '';
|
||||||
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, true);
|
// 会员图标不再挂头像角标,改到昵称行最前;禁封徽章依旧占据会员图标位。
|
||||||
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, false);
|
||||||
|
$name_badge = function_exists('aksrt_get_user_ban_name_badge') ? aksrt_get_user_ban_name_badge($user_id) : '';
|
||||||
|
if ($name_badge === '' && function_exists('aksrt_get_vip_icon')) {
|
||||||
|
$name_badge = aksrt_get_vip_icon($user_id);
|
||||||
|
}
|
||||||
|
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . $name_badge . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
||||||
|
|
||||||
// 每日签到奖励卡片(参考主题:积分徽章 + 经验值徽章,经验值随等级系统展示)
|
// 每日签到奖励卡片(参考主题:积分徽章 + 经验值徽章,经验值随等级系统展示)
|
||||||
$reward_badge = $base ? '<badge class=" p2-10 mr6 c-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('积分奖励%s', 'aksrt'), $base)) . '">' . $points_svg('icon') . '+' . esc_html((string) $base) . '</badge>' : '';
|
$reward_badge = $base ? '<badge class=" p2-10 mr6 c-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('积分奖励%s', 'aksrt'), $base)) . '">' . $points_svg('icon') . '+' . esc_html((string) $base) . '</badge>' : '';
|
||||||
|
|||||||
+279
-63
@@ -9,6 +9,14 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
class SearchPush {
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-358
@@ -1,23 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
/** User applications and administrator review for full order refunds. */
|
/** User applications and administrator status management for order after-sales. */
|
||||||
|
|
||||||
namespace AKSRT\Payment;
|
namespace AKSRT\Payment;
|
||||||
|
|
||||||
use AKSRT\Core\DomainModel;
|
|
||||||
use AKSRT\Ledger\AssetAmount;
|
|
||||||
use AKSRT\Ledger\AssetType;
|
|
||||||
use AKSRT\Ledger\LedgerCommand;
|
|
||||||
use AKSRT\Ledger\LedgerService;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
final class AfterSaleService {
|
final class AfterSaleService {
|
||||||
public const STATUS_PENDING = 'pending';
|
public const STATUS_PENDING = 'pending';
|
||||||
public const STATUS_PROCESSING = 'processing';
|
public const STATUS_PROCESSING = 'processing';
|
||||||
public const STATUS_APPROVED = 'approved';
|
|
||||||
public const STATUS_REJECTED = 'rejected';
|
public const STATUS_REJECTED = 'rejected';
|
||||||
public const STATUS_CANCELLED = 'cancelled';
|
public const STATUS_CANCELLED = 'cancelled';
|
||||||
public const STATUS_COMPLETED = 'completed';
|
public const STATUS_COMPLETED = 'completed';
|
||||||
|
/** 处理记录来源:后台提交 / 用户提交(申请与撤销)。 */
|
||||||
|
public const SOURCE_ADMIN = 'admin';
|
||||||
|
public const SOURCE_USER = 'user';
|
||||||
|
/** 处理记录上限:与 MetaAssetService::MAX_RECORDS 同口径,超出后只保留最近的一段。 */
|
||||||
|
private const MAX_LOG_ENTRIES = 50;
|
||||||
private static ?self $instance = null;
|
private static ?self $instance = null;
|
||||||
|
|
||||||
public static function get_instance(): self { return self::$instance ??= new self(); }
|
public static function get_instance(): self { return self::$instance ??= new self(); }
|
||||||
@@ -93,6 +91,10 @@ final class AfterSaleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function rest_submit(\WP_REST_Request $request) {
|
public function rest_submit(\WP_REST_Request $request) {
|
||||||
|
$captcha = $this->require_captcha($request, 'after_sale_apply');
|
||||||
|
if (is_wp_error($captcha)) {
|
||||||
|
return $captcha;
|
||||||
|
}
|
||||||
$images = $request->get_param('images');
|
$images = $request->get_param('images');
|
||||||
$materials = $request->get_param('materials');
|
$materials = $request->get_param('materials');
|
||||||
$result = $this->create(
|
$result = $this->create(
|
||||||
@@ -116,10 +118,23 @@ final class AfterSaleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function rest_cancel(\WP_REST_Request $request) {
|
public function rest_cancel(\WP_REST_Request $request) {
|
||||||
|
$captcha = $this->require_captcha($request, 'after_sale_cancel');
|
||||||
|
if (is_wp_error($captcha)) {
|
||||||
|
return $captcha;
|
||||||
|
}
|
||||||
$result = $this->cancel_for_user(get_current_user_id(), (string) $request->get_param('order_number'));
|
$result = $this->cancel_for_user(get_current_user_id(), (string) $request->get_param('order_number'));
|
||||||
return is_wp_error($result) ? $result : rest_ensure_response(['success' => true, 'data' => $result]);
|
return is_wp_error($result) ? $result : rest_ensure_response(['success' => true, 'data' => $result]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 人机验证(申请售后 / 撤销售后):场景未启用时 aksrt_require_captcha 直接放行。 */
|
||||||
|
private function require_captcha(\WP_REST_Request $request, string $scene) {
|
||||||
|
if (!function_exists('aksrt_require_captcha')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$params = $request->get_json_params();
|
||||||
|
return aksrt_require_captcha($scene, is_array($params) ? $params : []);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed> $apply_form */
|
/** @param array<string,mixed> $apply_form */
|
||||||
public function create(int $user_id, string $order_number, string $reason, array $images = [], array $apply_form = []) {
|
public function create(int $user_id, string $order_number, string $reason, array $images = [], array $apply_form = []) {
|
||||||
$reason = sanitize_textarea_field($reason);
|
$reason = sanitize_textarea_field($reason);
|
||||||
@@ -134,6 +149,8 @@ final class AfterSaleService {
|
|||||||
try {
|
try {
|
||||||
$order = OrderRepository::get_instance()->get_user_order($user_id, $order_number);
|
$order = OrderRepository::get_instance()->get_user_order($user_id, $order_number);
|
||||||
if (!$order || (string) ($order['status'] ?? '') !== Order::STATUS_PAID) return new \WP_Error('aksrt_after_sale_order', __('只有已支付订单可以申请售后', 'aksrt'), ['status' => 409]);
|
if (!$order || (string) ($order['status'] ?? '') !== Order::STATUS_PAID) return new \WP_Error('aksrt_after_sale_order', __('只有已支付订单可以申请售后', 'aksrt'), ['status' => 409]);
|
||||||
|
// 只挡「仍在流转」的工单。已完结(completed)的工单不挡——能否再售后由
|
||||||
|
// 「订单是否已退款」决定,而订单已退款时上面的 STATUS_PAID 校验已先挡掉。
|
||||||
$active = $this->find_active($order_number);
|
$active = $this->find_active($order_number);
|
||||||
if ($active) {
|
if ($active) {
|
||||||
if ((int) ($active['user_id'] ?? 0) === $user_id && hash_equals((string) ($active['reason'] ?? ''), $reason)) {
|
if ((int) ($active['user_id'] ?? 0) === $user_id && hash_equals((string) ($active['reason'] ?? ''), $reason)) {
|
||||||
@@ -143,13 +160,15 @@ final class AfterSaleService {
|
|||||||
}
|
}
|
||||||
$amount = $this->refund_amount($order);
|
$amount = $this->refund_amount($order);
|
||||||
if ($amount === null) return new \WP_Error('aksrt_after_sale_amount', __('订单退款金额无效', 'aksrt'), ['status' => 400]);
|
if ($amount === null) return new \WP_Error('aksrt_after_sale_amount', __('订单退款金额无效', 'aksrt'), ['status' => 400]);
|
||||||
|
$now = current_time('mysql');
|
||||||
$inserted = $wpdb->insert($this->table(), [
|
$inserted = $wpdb->insert($this->table(), [
|
||||||
'order_number' => $order_number, 'user_id' => $user_id, 'aftersale_type' => 'refund',
|
'order_number' => $order_number, 'user_id' => $user_id, 'aftersale_type' => 'refund',
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
'apply_form' => $apply_form === [] ? null : wp_json_encode($apply_form, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
'apply_form' => $apply_form === [] ? null : wp_json_encode($apply_form, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
'images' => wp_json_encode($images), 'refund_amount' => $amount,
|
'images' => wp_json_encode($images), 'refund_amount' => $amount,
|
||||||
'status' => self::STATUS_PENDING, 'created_at' => current_time('mysql'),
|
'status' => self::STATUS_PENDING, 'created_at' => $now,
|
||||||
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
'handling_log' => $this->encode_log([$this->log_entry(self::STATUS_PENDING, '', self::SOURCE_USER, $now)]),
|
||||||
|
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||||
return $inserted === 1 ? $this->get((int) $wpdb->insert_id) : new \WP_Error('aksrt_after_sale_db', __('售后申请保存失败', 'aksrt'), ['status' => 500]);
|
return $inserted === 1 ? $this->get((int) $wpdb->insert_id) : new \WP_Error('aksrt_after_sale_db', __('售后申请保存失败', 'aksrt'), ['status' => 500]);
|
||||||
} finally {
|
} finally {
|
||||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||||
@@ -202,6 +221,15 @@ final class AfterSaleService {
|
|||||||
return $clean;
|
return $clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款金额:积分单取整,现金单经 AssetAmount 归一为两位小数;
|
||||||
|
* 金额无法解析时返回 null,由 create() 拒掉这次申请。
|
||||||
|
*/
|
||||||
|
private function refund_amount(array $order): ?string {
|
||||||
|
if ((string) ($order['currency'] ?? '') === 'points') return (string) max(0, (int) ($order['amount'] ?? 0));
|
||||||
|
try { return \AKSRT\Ledger\AssetAmount::balance((string) ($order['amount'] ?? '0'))->decimal(); } catch (\InvalidArgumentException $e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
/** 最新一条售后工单(列表视图只取最新态,避免整表拉取)。 */
|
/** 最新一条售后工单(列表视图只取最新态,避免整表拉取)。 */
|
||||||
public function latest_for_order(string $order_number): ?array {
|
public function latest_for_order(string $order_number): ?array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -219,6 +247,7 @@ final class AfterSaleService {
|
|||||||
if (!$row || (int) ($row['user_id'] ?? 0) !== $user_id) return new \WP_Error('aksrt_after_sale_missing', __('没有可取消的售后申请', 'aksrt'), ['status' => 404]);
|
if (!$row || (int) ($row['user_id'] ?? 0) !== $user_id) return new \WP_Error('aksrt_after_sale_missing', __('没有可取消的售后申请', 'aksrt'), ['status' => 404]);
|
||||||
if ((string) ($row['status'] ?? '') !== self::STATUS_PENDING) return new \WP_Error('aksrt_after_sale_state', __('售后申请已进入处理阶段,无法取消', 'aksrt'), ['status' => 409]);
|
if ((string) ($row['status'] ?? '') !== self::STATUS_PENDING) return new \WP_Error('aksrt_after_sale_state', __('售后申请已进入处理阶段,无法取消', 'aksrt'), ['status' => 409]);
|
||||||
if (!$this->update((int) $row['id'], ['status' => self::STATUS_CANCELLED])) return new \WP_Error('aksrt_after_sale_db', __('取消售后申请失败', 'aksrt'), ['status' => 500]);
|
if (!$this->update((int) $row['id'], ['status' => self::STATUS_CANCELLED])) return new \WP_Error('aksrt_after_sale_db', __('取消售后申请失败', 'aksrt'), ['status' => 500]);
|
||||||
|
$this->append_log((int) $row['id'], $row, self::STATUS_CANCELLED, '', self::SOURCE_USER);
|
||||||
return $this->get((int) $row['id']);
|
return $this->get((int) $row['id']);
|
||||||
} finally {
|
} finally {
|
||||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||||
@@ -238,7 +267,7 @@ final class AfterSaleService {
|
|||||||
$where = [];
|
$where = [];
|
||||||
$params = [];
|
$params = [];
|
||||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||||
$valid = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED, self::STATUS_COMPLETED];
|
$valid = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_REJECTED, self::STATUS_CANCELLED, self::STATUS_COMPLETED];
|
||||||
if ($status !== '' && in_array($status, $valid, true)) { $where[] = 'status = %s'; $params[] = $status; }
|
if ($status !== '' && in_array($status, $valid, true)) { $where[] = 'status = %s'; $params[] = $status; }
|
||||||
foreach (['date_from' => '>=', 'date_to' => '<='] as $key => $operator) {
|
foreach (['date_from' => '>=', 'date_to' => '<='] as $key => $operator) {
|
||||||
$date = sanitize_text_field((string) ($args[$key] ?? ''));
|
$date = sanitize_text_field((string) ($args[$key] ?? ''));
|
||||||
@@ -274,7 +303,7 @@ final class AfterSaleService {
|
|||||||
public function count_user_open(int $user_id): int {
|
public function count_user_open(int $user_id): int {
|
||||||
if ($user_id <= 0) return 0;
|
if ($user_id <= 0) return 0;
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} WHERE user_id = %d AND status IN ('pending','processing','approved')", $user_id));
|
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} WHERE user_id = %d AND status IN ('pending','processing')", $user_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A refund status transition is valid only for its own active application. */
|
/** A refund status transition is valid only for its own active application. */
|
||||||
@@ -283,375 +312,75 @@ final class AfterSaleService {
|
|||||||
$row = $this->get($after_sale_id);
|
$row = $this->get($after_sale_id);
|
||||||
return is_array($row)
|
return is_array($row)
|
||||||
&& hash_equals((string) ($row['order_number'] ?? ''), $order_number)
|
&& hash_equals((string) ($row['order_number'] ?? ''), $order_number)
|
||||||
&& in_array((string) ($row['status'] ?? ''), [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED], true);
|
&& in_array((string) ($row['status'] ?? ''), [self::STATUS_PENDING, self::STATUS_PROCESSING], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function review(int $id, string $decision, string $note = '') {
|
/**
|
||||||
|
* 管理员直接设置售后工单状态:纯状态更新,不触发订单、资产与支付渠道联动。
|
||||||
|
*
|
||||||
|
* 每次提交都往 handling_log 落一条「状态 + 备注 + 来源 + 时间」,备注为空也留痕,
|
||||||
|
* 后台与前台才能回放整条处理链路。读取-追加-写回需要与 create()/cancel_for_user()
|
||||||
|
* 同一把订单锁,否则两条并发提交会互相覆盖日志。
|
||||||
|
*/
|
||||||
|
public function set_status(int $id, string $status, string $note = '') {
|
||||||
if (!current_user_can('manage_options')) return new \WP_Error('aksrt_after_sale_forbidden', __('权限不足', 'aksrt'), ['status' => 403]);
|
if (!current_user_can('manage_options')) return new \WP_Error('aksrt_after_sale_forbidden', __('权限不足', 'aksrt'), ['status' => 403]);
|
||||||
$decision = sanitize_key($decision);
|
$status = sanitize_key($status);
|
||||||
if (!in_array($decision, ['approve', 'reject', 'complete'], true)) return new \WP_Error('aksrt_after_sale_decision', __('审核动作无效', 'aksrt'), ['status' => 400]);
|
if (!in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_COMPLETED, self::STATUS_REJECTED, self::STATUS_CANCELLED], true)) return new \WP_Error('aksrt_after_sale_status', __('售后状态无效', 'aksrt'), ['status' => 400]);
|
||||||
|
$row = $this->get($id);
|
||||||
|
if (!$row) return new \WP_Error('aksrt_after_sale_state', __('售后申请不存在', 'aksrt'), ['status' => 409]);
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$initial = $this->get($id);
|
$lock = 'aksrt_after_sale_' . md5((string) ($row['order_number'] ?? ''));
|
||||||
if (!$initial) return new \WP_Error('aksrt_after_sale_state', __('售后申请已处理或不存在', 'aksrt'), ['status' => 409]);
|
if ((int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock)) !== 1) return new \WP_Error('aksrt_after_sale_busy', __('订单正在处理中,请稍后重试', 'aksrt'), ['status' => 409]);
|
||||||
$order_number = (string) ($initial['order_number'] ?? '');
|
|
||||||
$lock = 'aksrt_after_sale_' . md5($order_number);
|
|
||||||
if ((int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock)) !== 1) return new \WP_Error('aksrt_after_sale_busy', __('售后申请正在处理中,请稍后重试', 'aksrt'), ['status' => 409]);
|
|
||||||
try {
|
try {
|
||||||
|
// 锁内重读:追加必须基于最新快照,否则两条并发提交仍会互相覆盖日志。
|
||||||
$row = $this->get($id);
|
$row = $this->get($id);
|
||||||
if (!$row || !hash_equals($order_number, (string) ($row['order_number'] ?? '')) || !in_array((string) $row['status'], [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) return new \WP_Error('aksrt_after_sale_state', __('售后申请已处理或不存在', 'aksrt'), ['status' => 409]);
|
if (!$row) return new \WP_Error('aksrt_after_sale_state', __('售后申请不存在', 'aksrt'), ['status' => 409]);
|
||||||
$note = sanitize_textarea_field($note);
|
$note = sanitize_textarea_field($note);
|
||||||
if ((string) $row['status'] === self::STATUS_PROCESSING) {
|
if (!$this->update($id, ['status' => $status])) return new \WP_Error('aksrt_after_sale_db', __('售后状态保存失败', 'aksrt'), ['status' => 500]);
|
||||||
if ($decision === 'complete') {
|
$this->append_log($id, $row, $status, $note, self::SOURCE_ADMIN);
|
||||||
$gateway_result = json_decode((string) ($row['gateway_result'] ?? ''), true);
|
return $this->get($id);
|
||||||
if (!is_array($gateway_result) || sanitize_key((string) ($gateway_result['status'] ?? '')) !== 'manual') {
|
|
||||||
return new \WP_Error('aksrt_after_sale_manual_only', __('该申请不是待人工确认的退款', 'aksrt'), ['status' => 409]);
|
|
||||||
}
|
|
||||||
$order = OrderRepository::get_instance()->get((string) $row['order_number']);
|
|
||||||
if (!$order) return new \WP_Error('aksrt_after_sale_order', __('订单不存在', 'aksrt'), ['status' => 404]);
|
|
||||||
$finished = $this->finish_refund($row, $order, $note !== '' ? $note : (string) ($row['admin_note'] ?? ''));
|
|
||||||
return is_wp_error($finished) ? $finished : $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
if ($decision === 'reject') {
|
|
||||||
$gateway_result = json_decode((string) ($row['gateway_result'] ?? ''), true);
|
|
||||||
if (!is_array($gateway_result) || sanitize_key((string) ($gateway_result['status'] ?? '')) !== 'manual') {
|
|
||||||
return new \WP_Error('aksrt_after_sale_processing', __('退款处理中,不能拒绝;请同步退款结果', 'aksrt'), ['status' => 409]);
|
|
||||||
}
|
|
||||||
$order = OrderRepository::get_instance()->get((string) $row['order_number']);
|
|
||||||
if (!$order) return new \WP_Error('aksrt_after_sale_order', __('订单不存在', 'aksrt'), ['status' => 404]);
|
|
||||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
|
||||||
$revoke = $this->assets_to_revoke($order, $type, (array) ($order['meta_payload'] ?? []));
|
|
||||||
if (is_wp_error($revoke)) return $revoke;
|
|
||||||
if ($revoke !== []) {
|
|
||||||
$compensated = $this->compensate_revoke($row, $order, $revoke, $note);
|
|
||||||
if (is_wp_error($compensated)) return $compensated;
|
|
||||||
}
|
|
||||||
if (!$this->update((int) $row['id'], ['status' => self::STATUS_REJECTED, 'admin_note' => $note !== '' ? $note : __('管理员确认未执行渠道退款,已恢复冻结资产', 'aksrt')])) {
|
|
||||||
return new \WP_Error('aksrt_after_sale_db', __('售后审核保存失败', 'aksrt'), ['status' => 500]);
|
|
||||||
}
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
return $this->sync_gateway_refund($row, $note);
|
|
||||||
}
|
|
||||||
if ($decision === 'complete') return new \WP_Error('aksrt_after_sale_manual_only', __('只有待人工确认的退款可以直接完成', 'aksrt'), ['status' => 409]);
|
|
||||||
if ($decision === 'reject') {
|
|
||||||
if (!$this->update($id, ['status' => self::STATUS_REJECTED, 'admin_note' => $note, 'reviewed_at' => current_time('mysql')])) {
|
|
||||||
return new \WP_Error('aksrt_after_sale_db', __('售后审核保存失败', 'aksrt'), ['status' => 500]);
|
|
||||||
}
|
|
||||||
return $this->get($id);
|
|
||||||
}
|
|
||||||
return $this->approve($row, $note);
|
|
||||||
} finally {
|
} finally {
|
||||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function approve(array $row, string $note) {
|
/**
|
||||||
$order_number = (string) $row['order_number'];
|
* 追加一条处理记录。日志按时间正序存放,超出 MAX_LOG_ENTRIES 条时只保留最近的一段。
|
||||||
$order = OrderRepository::get_instance()->get($order_number);
|
*
|
||||||
if (!$order || (string) ($order['status'] ?? '') !== Order::STATUS_PAID) return new \WP_Error('aksrt_after_sale_order', __('订单已不是可退款状态', 'aksrt'), ['status' => 409]);
|
* @param array<string,mixed> $current 追加前的工单行(含已规范化的 handling_log)。
|
||||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
*/
|
||||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
private function append_log(int $id, array $current, string $status, string $note, string $source): void {
|
||||||
$method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
$log = is_array($current['handling_log'] ?? null) ? $current['handling_log'] : [];
|
||||||
if ($type === DomainModel::ORDER_PRODUCT_VIP && !$this->vip_reversal_is_safe($order, $meta)) return new \WP_Error('aksrt_after_sale_vip_snapshot', __('会员权益已变化或缺少快照,不能安全撤销', 'aksrt'), ['status' => 409]);
|
$log[] = $this->log_entry($status, $note, $source, current_time('mysql'));
|
||||||
$revoke = $this->assets_to_revoke($order, $type, $meta);
|
if (count($log) > self::MAX_LOG_ENTRIES) {
|
||||||
if (is_wp_error($revoke)) return $revoke;
|
$log = array_slice($log, -self::MAX_LOG_ENTRIES);
|
||||||
if ($method === 'card_pass') {
|
|
||||||
if (!$this->asset_entries_available($revoke)) return new \WP_Error('aksrt_after_sale_asset', __('当前资产不足,不能撤销卡密权益', 'aksrt'), ['status' => 409]);
|
|
||||||
return $this->commit_status_refund($row, $order, $note, $revoke);
|
|
||||||
}
|
}
|
||||||
if ($method === 'free') {
|
$this->update($id, ['handling_log' => $this->encode_log($log)]);
|
||||||
return $this->commit_status_refund($row, $order, $note, $revoke);
|
|
||||||
}
|
|
||||||
$refund_local = in_array($method, ['balance', 'points'], true);
|
|
||||||
if ($refund_local) {
|
|
||||||
$refund_asset = $method === 'points' ? AssetType::POINTS : AssetType::BALANCE;
|
|
||||||
$refund_minor = $refund_asset === AssetType::POINTS ? max(0, (int) ($order['amount'] ?? 0)) : $this->cash_minor((string) ($row['refund_amount'] ?? '0'));
|
|
||||||
if ($refund_minor <= 0) return new \WP_Error('aksrt_after_sale_amount', __('退款金额无效', 'aksrt'), ['status' => 400]);
|
|
||||||
$entries = $revoke;
|
|
||||||
// A points-purchase reversal already includes the original balance payment
|
|
||||||
// in assets_to_revoke(); do not return that payment twice.
|
|
||||||
if (!($type === DomainModel::ORDER_PRODUCT_POINTS && $method === 'balance')) {
|
|
||||||
$entries[] = ['user_id' => (int) $order['user_id'], 'asset' => $refund_asset->value, 'delta_minor' => $refund_minor, 'entry_type' => 'credit', 'remark' => sprintf(__('订单 %s 售后退款', 'aksrt'), $order_number)];
|
|
||||||
}
|
|
||||||
return $this->commit_local_refund($row, $order, $entries, $note);
|
|
||||||
}
|
|
||||||
$gateway = Gateway::get_instance()->get_gateway($method);
|
|
||||||
if (!$gateway || !method_exists($gateway, 'refund')) return new \WP_Error('aksrt_after_sale_gateway', __('支付渠道不可用,无法退款', 'aksrt'), ['status' => 409]);
|
|
||||||
$request_id = (string) ($row['gateway_request_id'] ?? '') ?: 'AS_' . (int) $row['id'];
|
|
||||||
if (!$this->asset_entries_available($revoke)) {
|
|
||||||
return new \WP_Error('aksrt_after_sale_asset', __('当前资产不足,不能审核通过退款', 'aksrt'), ['status' => 409]);
|
|
||||||
}
|
|
||||||
if (!$this->update((int) $row['id'], ['status' => self::STATUS_PROCESSING, 'admin_note' => $note, 'gateway_request_id' => $request_id, 'reviewed_at' => current_time('mysql')])) {
|
|
||||||
return new \WP_Error('aksrt_after_sale_db', __('售后状态保存失败,未发起退款', 'aksrt'), ['status' => 500]);
|
|
||||||
}
|
|
||||||
if ($revoke !== []) {
|
|
||||||
$held = $this->post_asset_entries($row, $order, $revoke, 'after-sale-revoke', 'after_sale_revoke', $note);
|
|
||||||
if (is_wp_error($held)) {
|
|
||||||
$this->update((int) $row['id'], ['status' => self::STATUS_PENDING, 'gateway_request_id' => null, 'reviewed_at' => null]);
|
|
||||||
return $held;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$result = $gateway->refund($order_number, (string) $row['refund_amount'], $request_id);
|
|
||||||
} catch (\Throwable $error) {
|
|
||||||
$result = ['success' => false, 'status' => 'unknown', 'message' => $error->getMessage()];
|
|
||||||
}
|
|
||||||
if (!is_array($result)) {
|
|
||||||
$result = ['success' => false, 'status' => 'unknown', 'message' => __('支付渠道未返回有效退款结果', 'aksrt')];
|
|
||||||
}
|
|
||||||
$this->update((int) $row['id'], ['gateway_result' => wp_json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'gateway_refund_id' => sanitize_text_field((string) ($result['refund_id'] ?? ''))]);
|
|
||||||
$status = sanitize_key((string) ($result['status'] ?? 'unknown'));
|
|
||||||
if ($status === 'failed') {
|
|
||||||
if ($revoke !== []) {
|
|
||||||
$compensated = $this->compensate_revoke($row, $order, $revoke, $note);
|
|
||||||
if (is_wp_error($compensated)) {
|
|
||||||
$this->update((int) $row['id'], ['admin_note' => __('渠道明确拒绝退款,但资产补偿失败;申请保持处理中,可再次同步重试补偿。', 'aksrt') . ' ' . $compensated->get_error_message()]);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->update((int) $row['id'], ['status' => self::STATUS_REJECTED, 'admin_note' => sanitize_textarea_field((string) ($result['message'] ?? $note))]);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
// Missing, unknown, and pending provider outcomes are ambiguous. Keep the
|
|
||||||
// application processing and the asset clawback in place until query_refund
|
|
||||||
// explicitly confirms success or failure.
|
|
||||||
if (empty($result['success']) || $status !== 'succeeded') {
|
|
||||||
$this->update((int) $row['id'], ['admin_note' => sanitize_textarea_field((string) ($result['message'] ?? __('退款结果仍待支付渠道确认,请稍后同步或人工核对', 'aksrt')))]);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
$finished = $this->finish_refund($row, $order, $note);
|
|
||||||
return is_wp_error($finished) ? $finished : $this->get((int) $row['id']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function commit_local_refund(array $row, array $order, array $entries, string $note) {
|
/** @return array<string,string> */
|
||||||
$number = (string) $order['order_number'];
|
private function log_entry(string $status, string $note, string $source, string $created_at): array {
|
||||||
$commission = CommissionLedger::get_instance()->prepare_full_refund($number);
|
return [
|
||||||
if (is_wp_error($commission)) return $commission;
|
'status' => $status,
|
||||||
$guard = static fn() => CommissionLedger::get_instance()->guard_full_refund($commission);
|
'note' => $note,
|
||||||
$mutation = function () use ($row, $order, $note, $commission) {
|
'source' => $source,
|
||||||
$commission_guard = CommissionLedger::get_instance()->guard_full_refund($commission);
|
'created_at' => $created_at,
|
||||||
if (is_wp_error($commission_guard) || $commission_guard !== true) return is_wp_error($commission_guard) ? $commission_guard : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]);
|
];
|
||||||
$finished = $this->finish_refund($row, $order, $note, false);
|
|
||||||
if ($finished === true) {
|
|
||||||
LedgerService::get_instance()->after_commit(static function (): void {
|
|
||||||
OrderRepository::get_instance()->dispatch_managed_status_events();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return $finished;
|
|
||||||
};
|
|
||||||
$result = LedgerService::get_instance()->post(new LedgerCommand(
|
|
||||||
\AKSRT\Ledger\OperationKey::deterministic('after-sale-refund', [(int) $row['id']]), 'order_refund', $entries,
|
|
||||||
'order', $number, get_current_user_id(), ['order_number' => $number]
|
|
||||||
), $guard, $mutation);
|
|
||||||
if (is_wp_error($result)) return $result;
|
|
||||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function post_asset_entries(array $row, array $order, array $entries, string $prefix, string $reason, string $note) {
|
private function encode_log(array $log): string {
|
||||||
return LedgerService::get_instance()->post(new LedgerCommand(
|
return wp_json_encode($log, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
\AKSRT\Ledger\OperationKey::deterministic($prefix, [(int) $row['id']]), $reason, $entries,
|
|
||||||
'order', (string) $order['order_number'], get_current_user_id(), ['order_number' => (string) $order['order_number'], 'after_sale_id' => (int) $row['id']]
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function commit_status_refund(array $row, array $order, string $note, array $entries = []) {
|
/** 该订单当前仍在处理中的售后工单(待审核/处理中),退款闸门只认它。 */
|
||||||
|
public function find_active(string $number): ?array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$number = (string) $order['order_number'];
|
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE order_number = %s AND status IN ('pending','processing') ORDER BY id DESC LIMIT 1", $number), ARRAY_A);
|
||||||
$commission = CommissionLedger::get_instance()->prepare_full_refund($number);
|
|
||||||
if (is_wp_error($commission)) return $commission;
|
|
||||||
if ($entries !== []) {
|
|
||||||
$mutation = function () use ($row, $order, $note, $commission) {
|
|
||||||
$guard = CommissionLedger::get_instance()->guard_full_refund($commission);
|
|
||||||
if (is_wp_error($guard) || $guard !== true) return is_wp_error($guard) ? $guard : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]);
|
|
||||||
$finished = $this->finish_refund($row, $order, $note, false);
|
|
||||||
if ($finished === true) {
|
|
||||||
LedgerService::get_instance()->after_commit(static function (): void { OrderRepository::get_instance()->dispatch_managed_status_events(); });
|
|
||||||
}
|
|
||||||
return $finished;
|
|
||||||
};
|
|
||||||
$result = LedgerService::get_instance()->post(new LedgerCommand(
|
|
||||||
\AKSRT\Ledger\OperationKey::deterministic('after-sale-card-refund', [(int) $row['id']]), 'order_refund', $entries,
|
|
||||||
'order', $number, get_current_user_id(), ['order_number' => $number]
|
|
||||||
), static fn() => CommissionLedger::get_instance()->guard_full_refund($commission), $mutation);
|
|
||||||
if (is_wp_error($result)) return $result;
|
|
||||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
if ($wpdb->query('START TRANSACTION') === false) return new \WP_Error('aksrt_after_sale_finish', __('无法开始退款状态更新', 'aksrt'), ['status' => 500]);
|
|
||||||
$guard = CommissionLedger::get_instance()->guard_full_refund($commission);
|
|
||||||
if (is_wp_error($guard) || $guard !== true) { $wpdb->query('ROLLBACK'); return is_wp_error($guard) ? $guard : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]); }
|
|
||||||
$finished = $this->finish_order_and_application($row, $order, $note);
|
|
||||||
if (is_wp_error($finished) || !$finished) { $wpdb->query('ROLLBACK'); return is_wp_error($finished) ? $finished : new \WP_Error('aksrt_after_sale_finish', __('退款状态保存失败', 'aksrt'), ['status' => 500]); }
|
|
||||||
$applied = CommissionLedger::get_instance()->apply_full_refund($commission);
|
|
||||||
if (is_wp_error($applied) || $wpdb->query('COMMIT') === false) { $wpdb->query('ROLLBACK'); return is_wp_error($applied) ? $applied : new \WP_Error('aksrt_after_sale_finish', __('退款状态保存失败', 'aksrt'), ['status' => 500]); }
|
|
||||||
OrderRepository::get_instance()->dispatch_managed_status_events();
|
|
||||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sync_gateway_refund(array $row, string $note) {
|
|
||||||
$order = OrderRepository::get_instance()->get((string) $row['order_number']);
|
|
||||||
$method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
|
||||||
$gateway = Gateway::get_instance()->get_gateway($method);
|
|
||||||
$request_id = (string) ($row['gateway_request_id'] ?? '');
|
|
||||||
if (!$order || !$gateway || !method_exists($gateway, 'query_refund') || $request_id === '') return new \WP_Error('aksrt_after_sale_sync_unavailable', __('该支付渠道暂不支持自动查询退款状态', 'aksrt'), ['status' => 409]);
|
|
||||||
try {
|
|
||||||
$result = $gateway->query_refund($request_id, (string) $row['order_number']);
|
|
||||||
} catch (\Throwable $error) {
|
|
||||||
$result = ['success' => false, 'status' => 'unknown', 'message' => $error->getMessage()];
|
|
||||||
}
|
|
||||||
if (!is_array($result)) {
|
|
||||||
$result = ['success' => false, 'status' => 'unknown', 'message' => __('支付渠道未返回有效退款结果', 'aksrt')];
|
|
||||||
}
|
|
||||||
$this->update((int) $row['id'], ['gateway_result' => wp_json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'gateway_refund_id' => sanitize_text_field((string) ($result['refund_id'] ?? $row['gateway_refund_id'] ?? ''))]);
|
|
||||||
$status = sanitize_key((string) ($result['status'] ?? 'unknown'));
|
|
||||||
if ($status === 'succeeded' && !empty($result['success'])) {
|
|
||||||
$finished = $this->finish_refund($row, $order, $note !== '' ? $note : (string) ($row['admin_note'] ?? ''));
|
|
||||||
return is_wp_error($finished) ? $finished : $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
if ($status === 'failed' && !empty($result['success'])) {
|
|
||||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
|
||||||
$revoke = $this->assets_to_revoke($order, $type, (array) ($order['meta_payload'] ?? []));
|
|
||||||
if (is_wp_error($revoke)) {
|
|
||||||
$this->update((int) $row['id'], ['admin_note' => __('渠道明确拒绝退款,但无法确认资产补偿内容;申请保持处理中,请人工核对。', 'aksrt') . ' ' . $revoke->get_error_message()]);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
if ($revoke !== []) {
|
|
||||||
$compensated = $this->compensate_revoke($row, $order, $revoke, $note);
|
|
||||||
if (is_wp_error($compensated)) {
|
|
||||||
$this->update((int) $row['id'], ['admin_note' => __('渠道明确拒绝退款,但资产补偿失败;申请保持处理中,可再次同步重试补偿。', 'aksrt') . ' ' . $compensated->get_error_message()]);
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->update((int) $row['id'], ['status' => self::STATUS_REJECTED, 'admin_note' => $note !== '' ? $note : __('支付渠道退款失败,已恢复撤销的资产', 'aksrt')]);
|
|
||||||
} elseif ($status !== 'succeeded') {
|
|
||||||
$this->update((int) $row['id'], ['admin_note' => $note !== '' ? $note : __('退款结果仍待支付渠道确认,请稍后同步或人工核对', 'aksrt')]);
|
|
||||||
}
|
|
||||||
return $this->get((int) $row['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function compensate_revoke(array $row, array $order, array $entries, string $note): bool|\WP_Error {
|
|
||||||
$compensate = array_map(static function (array $entry): array { $entry['delta_minor'] = -(int) $entry['delta_minor']; $entry['entry_type'] = 'credit'; $entry['remark'] = __('退款渠道失败,恢复已扣回资产', 'aksrt'); return $entry; }, $entries);
|
|
||||||
$result = $this->post_asset_entries($row, $order, $compensate, 'after-sale-revoke-compensate', 'after_sale_revoke_compensate', $note);
|
|
||||||
return is_wp_error($result) ? $result : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function assets_to_revoke(array $order, string $type, array $meta): array|\WP_Error {
|
|
||||||
$user_id = (int) ($order['user_id'] ?? 0);
|
|
||||||
if ($type === DomainModel::ORDER_PRODUCT_RECHARGE) {
|
|
||||||
$card_meta = is_array($meta['meta'] ?? null) ? $meta['meta'] : [];
|
|
||||||
try { $minor = AssetAmount::balance((string) ($meta['credited_amount'] ?? $meta['credit_amount'] ?? $card_meta['amount'] ?? ''))->minor(); } catch (\InvalidArgumentException $e) { return new \WP_Error('aksrt_after_sale_asset', __('充值到账金额缺失,不能退款', 'aksrt'), ['status' => 409]); }
|
|
||||||
return $this->debit_entry($user_id, AssetType::BALANCE, $minor, __('撤销充值余额', 'aksrt'));
|
|
||||||
}
|
|
||||||
if ($type === DomainModel::ORDER_PRODUCT_POINTS) {
|
|
||||||
$card_meta = is_array($meta['meta'] ?? null) ? $meta['meta'] : [];
|
|
||||||
$points = max(0, (int) ($meta['points'] ?? $card_meta['points'] ?? 0));
|
|
||||||
if ($points <= 0) return new \WP_Error('aksrt_after_sale_asset', __('购买积分数量缺失,不能退款', 'aksrt'), ['status' => 409]);
|
|
||||||
$entries = $this->debit_entry($user_id, AssetType::POINTS, $points, __('撤销订单发放积分', 'aksrt'));
|
|
||||||
if (sanitize_key((string) ($order['payment_method'] ?? '')) === 'balance') {
|
|
||||||
try { $paid = AssetAmount::balance((string) ($order['amount'] ?? ''))->minor(); } catch (\InvalidArgumentException $e) { return new \WP_Error('aksrt_after_sale_amount', __('积分订单支付金额无效', 'aksrt'), ['status' => 409]); }
|
|
||||||
$entries[] = ['user_id' => $user_id, 'asset' => AssetType::BALANCE->value, 'delta_minor' => $paid, 'entry_type' => 'credit', 'remark' => __('退回购买积分使用的余额', 'aksrt')];
|
|
||||||
}
|
|
||||||
return $entries;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function debit_entry(int $user_id, AssetType $asset, int $minor, string $remark): array|\WP_Error {
|
|
||||||
if ($user_id <= 0 || $minor <= 0) return new \WP_Error('aksrt_after_sale_asset', __('待撤销资产金额无效', 'aksrt'), ['status' => 409]);
|
|
||||||
return [['user_id' => $user_id, 'asset' => $asset->value, 'delta_minor' => -$minor, 'entry_type' => 'debit', 'remark' => $remark]];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function finish_refund(array $row, array $order, string $note, bool $manage_transaction = true) {
|
|
||||||
global $wpdb;
|
|
||||||
$number = (string) $order['order_number'];
|
|
||||||
$commission = CommissionLedger::get_instance()->prepare_full_refund($number);
|
|
||||||
if (is_wp_error($commission)) return $commission;
|
|
||||||
if ($manage_transaction && $wpdb->query('START TRANSACTION') === false) return new \WP_Error('aksrt_after_sale_finish', __('无法开始退款状态更新', 'aksrt'), ['status' => 500]);
|
|
||||||
$guarded = CommissionLedger::get_instance()->guard_full_refund($commission);
|
|
||||||
if (is_wp_error($guarded) || $guarded !== true) { if ($manage_transaction) $wpdb->query('ROLLBACK'); return is_wp_error($guarded) ? $guarded : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]); }
|
|
||||||
$finished = $this->finish_order_and_application($row, $order, $note);
|
|
||||||
if (is_wp_error($finished) || !$finished) { if ($manage_transaction) $wpdb->query('ROLLBACK'); return is_wp_error($finished) ? $finished : new \WP_Error('aksrt_after_sale_finish', __('退款成功但本地订单更新失败,请重试同步', 'aksrt'), ['status' => 500]); }
|
|
||||||
$applied = CommissionLedger::get_instance()->apply_full_refund($commission);
|
|
||||||
if (is_wp_error($applied) || ($manage_transaction && $wpdb->query('COMMIT') === false)) { if ($manage_transaction) $wpdb->query('ROLLBACK'); return is_wp_error($applied) ? $applied : new \WP_Error('aksrt_after_sale_finish', __('退款状态保存失败', 'aksrt'), ['status' => 500]); }
|
|
||||||
if ($manage_transaction) {
|
|
||||||
OrderRepository::get_instance()->dispatch_managed_status_events();
|
|
||||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function finish_order_and_application(array $row, array $order, string $note): bool|\WP_Error {
|
|
||||||
$number = (string) $order['order_number'];
|
|
||||||
if (!$this->revoke_vip_if_needed($order)) return new \WP_Error('aksrt_after_sale_vip', __('会员权益撤销失败', 'aksrt'), ['status' => 409]);
|
|
||||||
if (!OrderRepository::get_instance()->update_status($number, Order::STATUS_REFUNDED, ['allow_refund' => true, 'after_sale_id' => (int) $row['id'], 'transaction_managed' => true, 'note' => $note !== '' ? $note : __('售后退款完成', 'aksrt')])) return new \WP_Error('aksrt_after_sale_order', __('订单退款状态更新失败', 'aksrt'), ['status' => 409]);
|
|
||||||
return $this->update((int) $row['id'], ['status' => self::STATUS_COMPLETED, 'admin_note' => $note, 'completed_at' => current_time('mysql'), 'reviewed_at' => current_time('mysql')]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function vip_reversal_is_safe(array $order, array $meta): bool {
|
|
||||||
$before = is_array($meta['vip_before'] ?? null) ? $meta['vip_before'] : null;
|
|
||||||
$after = is_array($meta['vip_after'] ?? null) ? $meta['vip_after'] : null;
|
|
||||||
if (!$before || !$after || !class_exists('\\AKSRT\\Content\\VipService')) return false;
|
|
||||||
$current = \AKSRT\Content\VipService::get_instance()->get_membership_snapshot((int) $order['user_id']);
|
|
||||||
foreach (['level', 'expires', 'base_level', 'base_expires', 'expired_level'] as $key) {
|
|
||||||
if ((int) ($current[$key] ?? 0) !== (int) ($after[$key] ?? 0)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function revoke_vip_if_needed(array $order): bool {
|
|
||||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
|
||||||
if ($type !== DomainModel::ORDER_PRODUCT_VIP) return true;
|
|
||||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
|
||||||
if (!$this->vip_reversal_is_safe($order, $meta)) return false;
|
|
||||||
$before = $meta['vip_before'];
|
|
||||||
$service = \AKSRT\Content\VipService::get_instance();
|
|
||||||
if ((int) ($before['level'] ?? 0) <= 0) {
|
|
||||||
$result = $service->mutate_membership((int) $order['user_id'], 'disable', [], ['reason' => 'order_refund', 'source' => 'after_sale']);
|
|
||||||
} elseif ((int) ($before['base_level'] ?? 0) === 0 && (int) ($before['base_expires'] ?? 0) === 0 && (int) ($before['expired_level'] ?? 0) === 0) {
|
|
||||||
$result = $service->mutate_membership((int) $order['user_id'], 'set', ['level' => (int) $before['level'], 'expires' => (int) $before['expires']], ['reason' => 'order_refund', 'source' => 'after_sale']);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !is_wp_error($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function refund_amount(array $order): ?string {
|
|
||||||
if ((string) ($order['currency'] ?? '') === 'points') return (string) max(0, (int) ($order['amount'] ?? 0));
|
|
||||||
try { return AssetAmount::balance((string) ($order['amount'] ?? '0'))->decimal(); } catch (\InvalidArgumentException $e) { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cash_minor(string $value): int { try { return AssetAmount::balance($value)->minor(); } catch (\InvalidArgumentException $e) { return 0; } }
|
|
||||||
|
|
||||||
/** @param array<int,array<string,mixed>> $entries */
|
|
||||||
private function asset_entries_available(array $entries): bool {
|
|
||||||
$required = [];
|
|
||||||
foreach ($entries as $entry) {
|
|
||||||
$delta = (int) ($entry['delta_minor'] ?? 0);
|
|
||||||
if ($delta < 0) {
|
|
||||||
$key = (int) ($entry['user_id'] ?? 0) . ':' . (string) ($entry['asset'] ?? '');
|
|
||||||
$required[$key] = ($required[$key] ?? 0) + abs($delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($required as $key => $amount) {
|
|
||||||
[$user_id, $asset] = explode(':', $key, 2);
|
|
||||||
$balance = (string) $asset === AssetType::POINTS->value
|
|
||||||
? \AKSRT\Credits\MetaAssetService::get_instance()->balance_points((int) $user_id)
|
|
||||||
: \AKSRT\Credits\MetaAssetService::get_instance()->balance_minor((int) $user_id);
|
|
||||||
if ($balance < (int) $amount) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param array<int,array<string,mixed>> $entries */
|
|
||||||
private function find_active(string $number): ?array {
|
|
||||||
global $wpdb;
|
|
||||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE order_number = %s AND status IN ('pending','processing','approved') ORDER BY id DESC LIMIT 1", $number), ARRAY_A);
|
|
||||||
return is_array($row) ? $this->normalize($row) : null;
|
return is_array($row) ? $this->normalize($row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get(int $id): ?array {
|
/** 按 ID 读取售后申请(含关联订单号),供后台操作面板解析关联订单。 */
|
||||||
|
public function get(int $id): ?array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE id = %d", $id), ARRAY_A);
|
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE id = %d", $id), ARRAY_A);
|
||||||
return is_array($row) ? $this->normalize($row) : null;
|
return is_array($row) ? $this->normalize($row) : null;
|
||||||
@@ -675,6 +404,30 @@ final class AfterSaleService {
|
|||||||
: [];
|
: [];
|
||||||
$apply_form = json_decode((string) ($row['apply_form'] ?? ''), true);
|
$apply_form = json_decode((string) ($row['apply_form'] ?? ''), true);
|
||||||
$row['apply_form'] = is_array($apply_form) ? $apply_form : null;
|
$row['apply_form'] = is_array($apply_form) ? $apply_form : null;
|
||||||
|
$row['handling_log'] = $this->normalize_log(json_decode((string) ($row['handling_log'] ?? ''), true));
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理记录规范化:只保留 status 非空的条目,source 只认 user,其余归 admin;
|
||||||
|
* 非数组(含损坏 JSON)整体视为空,界面对老工单按「暂无处理记录」处理。
|
||||||
|
*
|
||||||
|
* @return list<array<string,string>>
|
||||||
|
*/
|
||||||
|
private function normalize_log($log): array {
|
||||||
|
if (!is_array($log)) return [];
|
||||||
|
$clean = [];
|
||||||
|
foreach ($log as $entry) {
|
||||||
|
if (!is_array($entry)) continue;
|
||||||
|
$status = sanitize_key((string) ($entry['status'] ?? ''));
|
||||||
|
if ($status === '') continue;
|
||||||
|
$clean[] = [
|
||||||
|
'status' => $status,
|
||||||
|
'note' => (string) ($entry['note'] ?? ''),
|
||||||
|
'source' => (string) ($entry['source'] ?? '') === self::SOURCE_USER ? self::SOURCE_USER : self::SOURCE_ADMIN,
|
||||||
|
'created_at' => (string) ($entry['created_at'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $clean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -709,62 +709,12 @@ trait OrderRepositoryCore {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string,mixed> */
|
public function dispatch_managed_status_events(): void {
|
||||||
public function get_admin_order_stats(array $args = []): array {
|
|
||||||
global $wpdb;
|
|
||||||
$this->expire_pending_orders();
|
|
||||||
$where = [];
|
|
||||||
$params = [];
|
|
||||||
foreach (['date_from' => '>=', 'date_to' => '<='] as $key => $operator) {
|
|
||||||
$date = sanitize_text_field((string) ($args[$key] ?? ''));
|
|
||||||
if ($date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
|
||||||
$where[] = 'created_at ' . $operator . ' %s';
|
|
||||||
$params[] = $operator === '>=' ? $date . ' 00:00:00' : $date . ' 23:59:59';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
||||||
$sql = "SELECT COUNT(*) AS total,
|
|
||||||
SUM(status = 'pending') AS pending,
|
|
||||||
SUM(status IN ('paid','refunding')) AS paid,
|
|
||||||
SUM(status = 'refunded') AS refunded,
|
|
||||||
SUM(status = 'closed') AS closed,
|
|
||||||
SUM(CASE WHEN status IN ('paid','refunding') AND currency <> 'points' THEN amount ELSE 0 END) AS revenue
|
|
||||||
FROM {$this->table()} {$where_sql}";
|
|
||||||
$row = $params ? $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_row($sql, ARRAY_A);
|
|
||||||
$type_sql = "SELECT product_type, note, COUNT(*) AS total FROM {$this->table()} {$where_sql} GROUP BY product_type, note";
|
|
||||||
$type_rows = $params ? $wpdb->get_results($wpdb->prepare($type_sql, $params), ARRAY_A) : $wpdb->get_results($type_sql, ARRAY_A);
|
|
||||||
$by_type = array_fill_keys(array_keys(\AKSRT\Core\DomainModel::order_product_type_labels()), 0);
|
|
||||||
foreach ((array) $type_rows as $type_row) {
|
|
||||||
$meta = json_decode((string) ($type_row['note'] ?? ''), true);
|
|
||||||
$type = \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($type_row['product_type'] ?? ''), is_array($meta) ? $meta : []);
|
|
||||||
if (isset($by_type[$type])) {
|
|
||||||
$by_type[$type] += (int) ($type_row['total'] ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'total' => (int) ($row['total'] ?? 0),
|
|
||||||
'pending' => (int) ($row['pending'] ?? 0),
|
|
||||||
'paid' => (int) ($row['paid'] ?? 0),
|
|
||||||
'refunded' => (int) ($row['refunded'] ?? 0),
|
|
||||||
'closed' => (int) ($row['closed'] ?? 0),
|
|
||||||
'revenue' => $this->cash_decimal((string) ($row['revenue'] ?? '0')),
|
|
||||||
'by_type' => $by_type,
|
|
||||||
'after_sale_pending' => class_exists(AfterSaleService::class) ? AfterSaleService::get_instance()->count([
|
|
||||||
'status' => AfterSaleService::STATUS_PENDING,
|
|
||||||
'date_from' => $args['date_from'] ?? '',
|
|
||||||
'date_to' => $args['date_to'] ?? '',
|
|
||||||
]) : 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dispatch_managed_status_events(): void {
|
|
||||||
$events = $this->status_after_commit_events;
|
$events = $this->status_after_commit_events;
|
||||||
$this->status_after_commit_events = [];
|
$this->status_after_commit_events = [];
|
||||||
$this->dispatch_status_events($events);
|
$this->dispatch_status_events($events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function has_paid_access(int $user_id, string $product_type, int $product_id): bool {
|
public function has_paid_access(int $user_id, string $product_type, int $product_id): bool {
|
||||||
if ($user_id <= 0 || $product_id <= 0) {
|
if ($user_id <= 0 || $product_id <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ class UserOrders {
|
|||||||
: [];
|
: [];
|
||||||
$settings = is_array($settings) ? $settings : [];
|
$settings = is_array($settings) ? $settings : [];
|
||||||
|
|
||||||
$open_statuses = ['pending', 'processing', 'approved'];
|
$open_statuses = ['pending', 'processing'];
|
||||||
$latest = null;
|
$latest = null;
|
||||||
$open = false;
|
$open = false;
|
||||||
if ($service !== null) {
|
if ($service !== null) {
|
||||||
@@ -287,12 +287,18 @@ class UserOrders {
|
|||||||
$has_application = $latest !== null;
|
$has_application = $latest !== null;
|
||||||
|
|
||||||
// 可再次申请售后的状态判定(按工单状态推理,不再以「不能再申请」为常态):
|
// 可再次申请售后的状态判定(按工单状态推理,不再以「不能再申请」为常态):
|
||||||
// - pending / processing / approved:工单仍在流转(待审核 / 处理中 / 已通过待退款),不允许重复申请;
|
// - pending / processing:工单仍在流转(待审核 / 处理中),不允许重复申请;
|
||||||
// - rejected:审核未通过,允许补充资料后再次申请;
|
// - rejected:审核未通过,允许补充资料后再次申请;
|
||||||
// - cancelled:用户自行取消,允许重新发起;
|
// - cancelled:用户自行取消,允许重新发起;
|
||||||
// - completed:退款已完成,工单终结,不允许再次申请。
|
// - completed:工单已完结,但只要订单没被另标「已退款」(enabled 仍为 true),
|
||||||
$reapplyable_statuses = [AfterSaleService::STATUS_REJECTED, AfterSaleService::STATUS_CANCELLED];
|
// 依旧允许再次申请。能否再售后由「订单是否已退款」决定,不由工单状态决定:
|
||||||
|
// 订单已退款时 $group 变成 refunded、enabled 直接为 false,上面那条自然挡死。
|
||||||
$latest_status = (string) ($latest['status'] ?? '');
|
$latest_status = (string) ($latest['status'] ?? '');
|
||||||
|
$reapplyable_statuses = [
|
||||||
|
AfterSaleService::STATUS_REJECTED,
|
||||||
|
AfterSaleService::STATUS_CANCELLED,
|
||||||
|
AfterSaleService::STATUS_COMPLETED,
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'enabled' => $enabled,
|
'enabled' => $enabled,
|
||||||
@@ -308,9 +314,8 @@ class UserOrders {
|
|||||||
'images' => array_values((array) ($latest['images'] ?? [])),
|
'images' => array_values((array) ($latest['images'] ?? [])),
|
||||||
'refund_amount' => (string) ($latest['refund_amount'] ?? '0'),
|
'refund_amount' => (string) ($latest['refund_amount'] ?? '0'),
|
||||||
'created_at' => (string) ($latest['created_at'] ?? ''),
|
'created_at' => (string) ($latest['created_at'] ?? ''),
|
||||||
'reviewed_at' => (string) ($latest['reviewed_at'] ?? ''),
|
// 处理记录:每次状态提交一条(状态 + 备注 + 来源 + 时间),前台时间线的唯一数据源。
|
||||||
'completed_at' => (string) ($latest['completed_at'] ?? ''),
|
'handling_log' => array_values((array) ($latest['handling_log'] ?? [])),
|
||||||
'admin_note' => (string) ($latest['admin_note'] ?? ''),
|
|
||||||
] : null,
|
] : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']),
|
||||||
|
|||||||
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user