Compare commits
87
Commits
9e3267fef4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef15416ef7 | ||
|
|
0cfd4c898b | ||
|
|
9a22c7eb8b | ||
|
|
de0e5cd481 | ||
|
|
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 } }) },
|
||||
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
||||
|
||||
/* AI 辅助 */
|
||||
getAiStatus() { return this.request('/ai/status') },
|
||||
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
|
||||
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
|
||||
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
|
||||
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
|
||||
}
|
||||
@@ -476,14 +476,43 @@
|
||||
|
||||
<!-- Fieldset (single nested object) -->
|
||||
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
||||
<AksField
|
||||
v-for="field in visibleNestedFields"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
:form-data="formData"
|
||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||
/>
|
||||
<template v-if="fieldsetSections.length">
|
||||
<div
|
||||
v-for="(section, sIndex) in fieldsetSections"
|
||||
:key="section.title"
|
||||
class="aksrt-field__accordion-section"
|
||||
:class="{ 'is-collapsed': !isFieldsetSectionOpen(sIndex) }"
|
||||
>
|
||||
<div class="aksrt-field__accordion-section-head" @click="toggleFieldsetSection(sIndex)">
|
||||
<span class="aksrt-field__accordion-section-title">{{ section.title }}</span>
|
||||
<FaIcon
|
||||
:icon="isFieldsetSectionOpen(sIndex) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
|
||||
:size="12"
|
||||
class="aksrt-field__accordion-section-toggle"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="isFieldsetSectionOpen(sIndex)" class="aksrt-field__accordion-section-body">
|
||||
<AksField
|
||||
v-for="field in sectionFields(section)"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
:form-data="formData"
|
||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!fieldsetSections.length">
|
||||
<AksField
|
||||
v-for="field in visibleNestedFields"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
:form-data="formData"
|
||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
||||
@@ -517,23 +546,38 @@
|
||||
animation="180"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="aksrt-field__repeater-item">
|
||||
<div class="aksrt-field__repeater-header">
|
||||
<div class="aksrt-field__repeater-item" :class="{ 'is-collapsible': repeaterCollapsible, 'is-collapsed': repeaterCollapsible && !isRepeaterItemOpen(index) }">
|
||||
<div
|
||||
class="aksrt-field__repeater-header"
|
||||
:class="{ 'is-clickable': repeaterCollapsible }"
|
||||
@click="repeaterCollapsible ? toggleRepeaterItem(index) : undefined"
|
||||
>
|
||||
<div class="aksrt-field__repeater-title">
|
||||
<i class="fa fas fa-grip-vertical aksrt-field__repeater-handle" aria-hidden="true"></i>
|
||||
<i class="fa fas fa-grip-vertical aksrt-field__repeater-handle" aria-hidden="true" @click.stop></i>
|
||||
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
|
||||
<span v-if="repeaterCollapsible && repeaterItemSummary(element)" class="aksrt-field__repeater-summary">
|
||||
{{ repeaterItemSummary(element) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="aksrt-field__repeater-header-actions" @click.stop>
|
||||
<FaIcon
|
||||
v-if="repeaterCollapsible"
|
||||
:icon="isRepeaterItemOpen(index) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
|
||||
:size="12"
|
||||
class="aksrt-field__repeater-toggle"
|
||||
/>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
text
|
||||
:disabled="schema.disabled || reachedRepeaterMinimum"
|
||||
@click="removeRepeaterItem(index)"
|
||||
>
|
||||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||||
</n-button>
|
||||
</div>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
text
|
||||
:disabled="schema.disabled || reachedRepeaterMinimum"
|
||||
@click="removeRepeaterItem(index)"
|
||||
>
|
||||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="aksrt-field__repeater-fields">
|
||||
<div v-show="!repeaterCollapsible || isRepeaterItemOpen(index)" class="aksrt-field__repeater-fields">
|
||||
<template v-for="field in (schema.fields || [])" :key="field.key">
|
||||
<AksField
|
||||
v-if="matchDependency(field.dependsOn, element)"
|
||||
@@ -697,6 +741,7 @@ import FaIconPicker from './FaIconPicker.vue'
|
||||
import AksMediaField from './AksMediaField.vue'
|
||||
import AksGalleryField from './AksGalleryField.vue'
|
||||
import AksCodeEditor from './AksCodeEditor.vue'
|
||||
import { buildFieldsetSections } from './fieldset-sections.js'
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
|
||||
@@ -730,6 +775,60 @@ const repeaterItems = computed(() => Array.isArray(props.modelValue) ? props.mod
|
||||
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
|
||||
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 手风琴模式:schema.collapsible 开启后,每一项默认收起,仅显示标题行,
|
||||
* 点击标题行展开/收起。defaultCollapsed 为 false 时改为默认全部展开。
|
||||
* ------------------------------------------------------------------ */
|
||||
const repeaterCollapsible = computed(() => !!props.schema.collapsible)
|
||||
const repeaterDefaultOpen = computed(() => props.schema.defaultCollapsed === false)
|
||||
// 只记录「与默认状态相反」的下标,新增/删除项时其余项的开合状态不受影响。
|
||||
const toggledRepeaterItems = ref(new Set())
|
||||
|
||||
function isRepeaterItemOpen(index) {
|
||||
if (!repeaterCollapsible.value) return true
|
||||
return repeaterDefaultOpen.value !== toggledRepeaterItems.value.has(index)
|
||||
}
|
||||
|
||||
function toggleRepeaterItem(index) {
|
||||
const next = new Set(toggledRepeaterItems.value)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
next.add(index)
|
||||
}
|
||||
toggledRepeaterItems.value = next
|
||||
}
|
||||
|
||||
/** 收起态标题行上的一行摘要,取自 listPreview 配置。 */
|
||||
function repeaterItemSummary(item) {
|
||||
if (!repeaterCollapsible.value || !item) return ''
|
||||
const cfg = getRepeaterPreviewConfig()
|
||||
const titleKey = cfg.titleKey || 'title'
|
||||
const title = String(item[titleKey] ?? '').trim()
|
||||
const prefix = String(cfg.prefix || '')
|
||||
if (title === '') return prefix
|
||||
const desc = repeaterItemDuration(item)
|
||||
return desc ? `${prefix}${title} · ${desc}` : `${prefix}${title}`
|
||||
}
|
||||
|
||||
/** time/unit 形态的时长摘要:0 视为永久,其余按 day/month 补单位。 */
|
||||
function repeaterItemDuration(item) {
|
||||
const cfg = getRepeaterPreviewConfig()
|
||||
const descKey = cfg.descKey
|
||||
if (!descKey) return ''
|
||||
const raw = item[descKey]
|
||||
if (raw === '' || raw == null) return ''
|
||||
const amount = Number(raw)
|
||||
if (!Number.isFinite(amount)) return String(raw)
|
||||
if (amount <= 0) return '永久'
|
||||
const unitKey = cfg.descUnitKey
|
||||
if (!unitKey) return String(raw)
|
||||
const unit = String(item[unitKey] || '')
|
||||
if (unit === 'month') return `${amount} 个月`
|
||||
if (unit === 'day') return `${amount} 天`
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
/** 拖拽视图:vuedraggable 以 modelValue 模式工作(内部复制后回传 update:modelValue)。 */
|
||||
const repeaterDragList = computed({
|
||||
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
|
||||
@@ -762,6 +861,35 @@ function fieldsetValue(field) {
|
||||
return Object.prototype.hasOwnProperty.call(current, field.key) ? current[field.key] : field.default
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* accordion/fieldset 的分块手风琴:schema.sections 声明每块的标题与字段
|
||||
* key,默认全部收起,点击块标题展开。未声明 sections 时保持平铺渲染。
|
||||
* ------------------------------------------------------------------ */
|
||||
const fieldsetSections = computed(() => {
|
||||
return buildFieldsetSections(props.schema)
|
||||
})
|
||||
|
||||
const openFieldsetSections = ref(new Set())
|
||||
|
||||
function isFieldsetSectionOpen(index) {
|
||||
return openFieldsetSections.value.has(index)
|
||||
}
|
||||
|
||||
/** 分块内字段同样遵循 dependsOn,数据源是本 fieldset 的值对象。 */
|
||||
function sectionFields(section) {
|
||||
return (section.fields || []).filter((field) => matchDependency(field.dependsOn, props.modelValue))
|
||||
}
|
||||
|
||||
function toggleFieldsetSection(index) {
|
||||
const next = new Set(openFieldsetSections.value)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
next.add(index)
|
||||
}
|
||||
openFieldsetSections.value = next
|
||||
}
|
||||
|
||||
function updateFieldsetValue(key, value) {
|
||||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||||
? props.modelValue
|
||||
@@ -1053,6 +1181,8 @@ const repeaterDraft = ref({})
|
||||
function createRepeaterItem() {
|
||||
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
||||
for (const field of (props.schema.fields || [])) {
|
||||
// UI-only chrome rows (notice / alert) are presentation only.
|
||||
if (field.ui_only) continue
|
||||
if (field.default !== undefined) {
|
||||
newItem[field.key] = field.default
|
||||
} else if (field.type === 'switch') {
|
||||
@@ -1082,7 +1212,16 @@ function removeRepeaterItem(index) {
|
||||
|
||||
function updateRepeaterItem(index, fieldKey, value) {
|
||||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||||
current[index] = { ...current[index], [fieldKey]: value }
|
||||
const row = current[index]
|
||||
if (row && typeof row === 'object') {
|
||||
// 必须原地写,不能展开成新对象:drag key 存在以「对象身份」为键的 WeakMap
|
||||
// 里,而行数据是 reactive 的——换一个新对象后,渲染时读到的是它的一个新
|
||||
// reactive 代理,WeakMap 认不出它便生成新 key,整行重挂载、输入框失焦,
|
||||
// 表现为打一个字就退出编辑。经代理原地写既保持身份,也照样触发响应式。
|
||||
row[fieldKey] = value
|
||||
} else {
|
||||
current[index] = { [fieldKey]: value }
|
||||
}
|
||||
emit('update:modelValue', current)
|
||||
}
|
||||
|
||||
@@ -1184,6 +1323,15 @@ function confirmRepeaterModal() {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
|
||||
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
|
||||
.aksrt-field--group,
|
||||
.aksrt-field--repeater,
|
||||
.aksrt-field--accordion,
|
||||
.aksrt-field--fieldset {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.aksrt-field__input-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -1449,6 +1597,48 @@ function confirmRepeaterModal() {
|
||||
background: var(--aksrt-color-canvas);
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section + .aksrt-field__accordion-section {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--aksrt-color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--aksrt-color-surface);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--aksrt-color-text);
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-head:hover {
|
||||
border-color: var(--aksrt-color-primary);
|
||||
color: var(--aksrt-color-primary);
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-toggle {
|
||||
flex-shrink: 0;
|
||||
color: var(--aksrt-color-text-secondary);
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 2px 2px;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1477,6 +1667,47 @@ function confirmRepeaterModal() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-header.is-clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 6px;
|
||||
margin: -4px -4px 0;
|
||||
padding: 4px 4px 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-header.is-clickable:hover .aksrt-field__repeater-index {
|
||||
color: var(--aksrt-color-primary-hover);
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-toggle {
|
||||
color: var(--aksrt-color-text-secondary);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-summary {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: var(--aksrt-color-text-secondary);
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-item.is-collapsible {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-item.is-collapsed .aksrt-field__repeater-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-handle {
|
||||
cursor: grab;
|
||||
color: var(--aksrt-color-text-tertiary);
|
||||
|
||||
@@ -64,9 +64,9 @@
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane
|
||||
v-for="tab in field.tabs"
|
||||
:key="tab.title || tab.key"
|
||||
:name="tab.title || tab.key"
|
||||
:tab="tab.title || tab.key"
|
||||
:key="tab.key || tab.title"
|
||||
:name="tabName(tab)"
|
||||
:tab="tabName(tab)"
|
||||
>
|
||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||
<AksField
|
||||
@@ -231,6 +231,18 @@ function isFieldVisible(field) {
|
||||
return matchDependsOn(field.dependsOn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
|
||||
* display name) so renaming a level renames its tab; `titleFallback` is used
|
||||
* while that value is still empty.
|
||||
*/
|
||||
function tabName(tab) {
|
||||
if (!tab) return ''
|
||||
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
|
||||
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
|
||||
return label || tab.titleFallback || tab.title || tab.key || ''
|
||||
}
|
||||
|
||||
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||
function isGroupVisible(group) {
|
||||
return matchDependsOn(group.dependsOn)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 把 accordion / fieldset 的 `schema.sections` 声明展开成「块标题 + 字段定义」。
|
||||
*
|
||||
* 参考主题把「升级价格」这类复合配置拆成若干可折叠块,每块用 keys 声明自己
|
||||
* 包含哪些字段。这里是纯函数,方便在 Node 契约测试里直接用真实 schema 校验,
|
||||
* 避免渲染层的一个小疏忽让整块手风琴静默退化成平铺。
|
||||
*/
|
||||
export function buildFieldsetSections(schema) {
|
||||
const declared = schema && Array.isArray(schema.sections) ? schema.sections : []
|
||||
if (!declared.length) return []
|
||||
|
||||
const byKey = {}
|
||||
const nested = Array.isArray(schema.fields) ? schema.fields : []
|
||||
for (const field of nested) {
|
||||
if (field && field.key) byKey[field.key] = field
|
||||
}
|
||||
|
||||
return declared.map((section) => ({
|
||||
title: String((section && section.title) || ''),
|
||||
fields: (Array.isArray(section && section.keys) ? section.keys : [])
|
||||
.map((key) => byKey[key])
|
||||
.filter(Boolean),
|
||||
}))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderFaIcon, schemaPage, customPage } from './helpers'
|
||||
import { renderFaIcon, schemaPage } from './helpers'
|
||||
|
||||
export const extensionsSection = {
|
||||
key: 'extensions',
|
||||
@@ -42,7 +42,7 @@ export const extensionsSection = {
|
||||
label: '百度推送',
|
||||
route: '/extensions/push',
|
||||
title: '百度推送',
|
||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
||||
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||
canonicalDomain: 'extensions',
|
||||
section: 'push',
|
||||
}),
|
||||
@@ -56,15 +56,5 @@ export const extensionsSection = {
|
||||
canonicalDomain: 'extensions',
|
||||
section: 'security',
|
||||
}),
|
||||
customPage({
|
||||
key: 'extensions-ai',
|
||||
name: 'ExtensionAI',
|
||||
label: 'AI 连接配置',
|
||||
route: '/extensions/ai',
|
||||
component: 'ExtensionAI',
|
||||
status: 'available',
|
||||
title: 'AI 连接配置',
|
||||
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -108,16 +108,6 @@ export const usersSection = {
|
||||
canonicalDomain: 'users',
|
||||
section: 'sms',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'users-wechat',
|
||||
name: 'UsersWechat',
|
||||
label: '微信通知',
|
||||
route: '/users/wechat',
|
||||
title: '微信通知',
|
||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
||||
canonicalDomain: 'users',
|
||||
section: 'wechat',
|
||||
}),
|
||||
customPage({
|
||||
key: 'users-wechat-template',
|
||||
name: 'UsersWechatTemplate',
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
|
||||
|
||||
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
|
||||
<strong>加载失败:</strong>{{ loadError }}
|
||||
</div>
|
||||
|
||||
<div class="aksrt-ai__status">
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
||||
<i class="fa fab fa-android"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">可用状态</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fa fas fa-plug"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">WP AI Client</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fa fas fa-key"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">主题 Provider</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
||||
<i class="fa fas fa-road"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">当前后端</div>
|
||||
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aksrt-ai__form-card">
|
||||
<h3>主题 Provider 配置(回退链路)</h3>
|
||||
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
|
||||
<div class="aksrt-form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
||||
</label>
|
||||
</div>
|
||||
<div class="aksrt-form-group">
|
||||
<label>AI Provider</label>
|
||||
<select v-model="form.provider" class="aksrt-select">
|
||||
<option value="none">不使用</option>
|
||||
<option value="openai">OpenAI 兼容接口</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>API Key</label>
|
||||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
||||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
||||
<label>API Base URL</label>
|
||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://llm.example.com/v1">
|
||||
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>模型</label>
|
||||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>最大 Token 数</label>
|
||||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>每分钟请求限制</label>
|
||||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
||||
</div>
|
||||
<div class="aksrt-form-actions">
|
||||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
||||
{{ saving ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aksrt-ai__form-card">
|
||||
<h3>已接通能力</h3>
|
||||
<table class="aksrt-table aksrt-table--sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>能力</th>
|
||||
<th>入口</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feat in features" :key="feat.key">
|
||||
<td>{{ feat.name }}</td>
|
||||
<td>{{ feat.scene }}</td>
|
||||
<td>
|
||||
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
||||
{{ feat.ready ? '可用' : '未就绪' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
|
||||
</div>
|
||||
|
||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
||||
<h3><i class="fa fas fa-exclamation-triangle"></i> 重要说明</h3>
|
||||
<ul>
|
||||
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
|
||||
<li>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
|
||||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import PageHeader from '@/components/PageHeader.vue';
|
||||
import api from '@/api';
|
||||
|
||||
export default {
|
||||
name: 'ExtensionAI',
|
||||
components: { PageHeader },
|
||||
setup() {
|
||||
const status = ref({
|
||||
enabled: false,
|
||||
provider: 'none',
|
||||
configured: false,
|
||||
model: '',
|
||||
api_key: '',
|
||||
rate_limit: 10,
|
||||
available: false,
|
||||
backend: 'none',
|
||||
wp_ai_available: false,
|
||||
theme_available: false,
|
||||
seo_ai_enabled: false,
|
||||
write_assist: false,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
enabled: false,
|
||||
provider: 'none',
|
||||
api_key: '',
|
||||
api_base: 'https://api.openai.com/v1',
|
||||
model: 'gpt-3.5-turbo',
|
||||
max_tokens: 1000,
|
||||
rate_limit: 10,
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
const loadError = ref('');
|
||||
|
||||
const backendLabel = computed(() => {
|
||||
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
|
||||
return map[status.value.backend] || status.value.backend || '无';
|
||||
});
|
||||
|
||||
const features = computed(() => {
|
||||
const avail = !!status.value.available;
|
||||
const seoReady = avail && !!status.value.seo_ai_enabled;
|
||||
return [
|
||||
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
|
||||
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
|
||||
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
|
||||
];
|
||||
});
|
||||
|
||||
function hydrateFromStatus(data) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
status.value = {
|
||||
enabled: !!data.enabled,
|
||||
provider: data.provider || 'none',
|
||||
configured: !!data.configured,
|
||||
model: data.model || '',
|
||||
api_key: '',
|
||||
rate_limit: data.rate_limit || 10,
|
||||
available: !!data.available,
|
||||
backend: data.backend || 'none',
|
||||
wp_ai_available: !!data.wp_ai_available,
|
||||
theme_available: !!data.theme_available,
|
||||
seo_ai_enabled: !!data.seo_ai_enabled,
|
||||
write_assist: !!data.write_assist,
|
||||
};
|
||||
form.enabled = !!data.enabled;
|
||||
form.provider = data.provider || 'none';
|
||||
form.model = data.model || 'gpt-3.5-turbo';
|
||||
form.rate_limit = data.rate_limit || 10;
|
||||
if (data.api_base) form.api_base = data.api_base;
|
||||
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
||||
form.max_tokens = data.max_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
loadError.value = '';
|
||||
try {
|
||||
const json = await api.getAiStatus();
|
||||
hydrateFromStatus(json.data || {});
|
||||
} catch (e) {
|
||||
console.error('Load AI status failed:', e);
|
||||
loadError.value = e?.message || '加载 AI 状态失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
enabled: form.enabled,
|
||||
provider: form.provider,
|
||||
api_base: form.api_base,
|
||||
model: form.model,
|
||||
max_tokens: form.max_tokens,
|
||||
rate_limit: form.rate_limit,
|
||||
};
|
||||
// Only send api_key when the user entered a new one.
|
||||
if (form.api_key && form.api_key !== '***') {
|
||||
payload.api_key = form.api_key;
|
||||
}
|
||||
const json = await api.updateAiSettings(payload);
|
||||
alert(json.message || '设置已保存');
|
||||
await loadStatus();
|
||||
} catch (e) {
|
||||
console.error('Save failed:', e);
|
||||
alert(e?.message || '保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadStatus());
|
||||
|
||||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-ai__status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.aksrt-ai__status-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
.aksrt-ai__status-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
|
||||
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
|
||||
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
|
||||
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
|
||||
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: var(--aksrt-color-text); }
|
||||
.aksrt-ai__status-label { font-size: 12px; color: #888; }
|
||||
.aksrt-ai__form-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
|
||||
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
.aksrt-ai__load-error {
|
||||
background: #fdecea;
|
||||
border-color: #f5c2c0;
|
||||
color: #842029;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
|
||||
.aksrt-ai__notice h3 { color: #e65100; }
|
||||
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
|
||||
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
|
||||
.aksrt-ai__notice code {
|
||||
background: rgba(0,0,0,0.06);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
|
||||
.aksrt-badge--success { background: #d4edda; color: #155724; }
|
||||
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
|
||||
.aksrt-table { width: 100%; border-collapse: collapse; }
|
||||
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
||||
.aksrt-form-group { margin-bottom: 14px; }
|
||||
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
|
||||
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
|
||||
.aksrt-button--primary { background: #1677ff; color: #fff; }
|
||||
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
export const extensionsPageLoaders = {
|
||||
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { corePageLoaders } from './core'
|
||||
import { usersPageLoaders } from './users'
|
||||
import { toolsPageLoaders } from './tools'
|
||||
import { extensionsPageLoaders } from './extensions'
|
||||
|
||||
export const componentLoaders = {
|
||||
...corePageLoaders,
|
||||
...usersPageLoaders,
|
||||
...toolsPageLoaders,
|
||||
...extensionsPageLoaders,
|
||||
}
|
||||
@@ -2,21 +2,16 @@ export const pushSchema = {
|
||||
key: 'extensions.push',
|
||||
title: '百度推送',
|
||||
icon: 'fa fas fa-paper-plane',
|
||||
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
||||
description: '配置百度主动推送、收录通道和推送状态。',
|
||||
defaults: {
|
||||
baidu_push_enabled: false,
|
||||
baidu_push_token: '',
|
||||
baidu_push_auto: true,
|
||||
baidu_push_on_publish: true,
|
||||
baidu_push_on_update: false,
|
||||
baidu_push_batch_size: 50,
|
||||
baidu_push_normal: true,
|
||||
baidu_push_fast: false,
|
||||
baidu_push_log_enabled: true,
|
||||
xzh_on: false,
|
||||
xzh_appid: '',
|
||||
xzh_render_tail: true,
|
||||
xzh_jsonld_single: true,
|
||||
xzh_jsonld_page: false,
|
||||
xzh_jsonld_img: false,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
@@ -25,7 +20,7 @@ export const pushSchema = {
|
||||
icon: 'fa fas fa-rocket',
|
||||
fields: [
|
||||
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
||||
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' },
|
||||
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -34,21 +29,17 @@ export const pushSchema = {
|
||||
icon: 'fa fas fa-bolt',
|
||||
fields: [
|
||||
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
||||
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
||||
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
||||
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' },
|
||||
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'xzh',
|
||||
title: '熊掌号 / JSON-LD',
|
||||
icon: 'fa fas fa-paw',
|
||||
key: 'channel',
|
||||
title: '收录通道',
|
||||
icon: 'fa fas fa-inbox',
|
||||
fields: [
|
||||
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
|
||||
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
|
||||
{ key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
||||
{ key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
||||
{ key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
||||
{ key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
||||
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
|
||||
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -56,8 +47,7 @@ export const pushSchema = {
|
||||
title: '高级设置',
|
||||
icon: 'fa fas fa-cog',
|
||||
fields: [
|
||||
{ key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@ export const seoSchema = {
|
||||
key: 'global.seo',
|
||||
title: 'SEO 优化',
|
||||
icon: 'fa fas fa-search',
|
||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框;连接配置在「扩展 → AI 连接配置」。优先使用 WordPress AI Connectors。',
|
||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框,连接由 WordPress AI Connectors 提供。',
|
||||
defaults: {
|
||||
seo_enabled: true,
|
||||
seo_ai_enabled: false,
|
||||
@@ -13,10 +13,6 @@ export const seoSchema = {
|
||||
ai_seo_title_prompt: '',
|
||||
ai_seo_keywords_prompt: '',
|
||||
ai_seo_description_prompt: '',
|
||||
seo_baidu_push: false,
|
||||
seo_baidu_normal: false,
|
||||
seo_baidu_fast: false,
|
||||
seo_baidu_token: '',
|
||||
seo_image_optimize: true,
|
||||
seo_image_default: '',
|
||||
seo_image_always_default: false,
|
||||
@@ -48,7 +44,7 @@ export const seoSchema = {
|
||||
label: '启用 AI SEO 生成',
|
||||
type: 'switch',
|
||||
default: false,
|
||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。',
|
||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先在 WordPress 配置 AI Connectors。',
|
||||
dependsOn: { key: 'seo_enabled', value: true },
|
||||
},
|
||||
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
||||
@@ -95,17 +91,6 @@ export const seoSchema = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'baidu',
|
||||
title: '百度推送',
|
||||
icon: 'fa fas fa-paper-plane',
|
||||
fields: [
|
||||
{ key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
|
||||
{ key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
|
||||
{ key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
|
||||
{ key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
title: '图片 SEO',
|
||||
|
||||
@@ -111,7 +111,7 @@ export const rechargeSchema = {
|
||||
{ key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' },
|
||||
{ key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' },
|
||||
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,27 +1,127 @@
|
||||
/**
|
||||
* VIP会员 — 参考主题结构:会员等级、会员权益(普通/一级/二级 tab)、
|
||||
* 会员购买选项(一级/二级/续费/升级 tab)、卡密兑换与积分兑换。
|
||||
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
|
||||
* (普通/一级/二级 tab)、会员购买选项(一级/二级/续费/升级 tab)、
|
||||
* 积分兑换会员与界面文案。
|
||||
*
|
||||
* 会员商品 / 续费商品 / 升级价格 / 积分兑换明细均为结构化 group 或
|
||||
* accordion,不再使用 `|` 分隔的 textarea。等级 Tab 标题通过 titleKey
|
||||
* 绑定「会员显示名称」,改名即改 Tab 名。
|
||||
*/
|
||||
|
||||
const VIP_EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||
|
||||
const vipTagPalette = [
|
||||
{ label: '#f74b3d', value: 'b-red' },
|
||||
{ label: '#f3920a', value: 'b-yellow' },
|
||||
{ label: '#08c4c1', value: 'b-cyan' },
|
||||
{ label: '#0a8cf3', value: 'b-blue' },
|
||||
{ label: '#1fd05a', value: 'b-green' },
|
||||
{ label: '#c133f5', value: 'b-purple' },
|
||||
{ label: '#121517', value: 'b-black' },
|
||||
{ label: 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)', value: 'jb-red' },
|
||||
{ label: 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)', value: 'jb-pink' },
|
||||
{ label: 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)', value: 'jb-yellow' },
|
||||
{ label: 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)', value: 'jb-cyan' },
|
||||
{ label: 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)', value: 'jb-blue' },
|
||||
{ label: 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)', value: 'jb-green' },
|
||||
{ label: 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)', value: 'jb-purple' },
|
||||
{ label: 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)', value: 'jb-vip1' },
|
||||
{ label: 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)', value: 'jb-vip2' },
|
||||
];
|
||||
|
||||
const vipUnitOptions = [
|
||||
{ label: '天', value: 'day' },
|
||||
{ label: '月', value: 'month' },
|
||||
];
|
||||
|
||||
// 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||
const vipProductGroupOptions = {
|
||||
collapsible: true,
|
||||
defaultCollapsed: true,
|
||||
listPreview: { prefix: '价格:', titleKey: 'price', descKey: 'time', descUnitKey: 'unit' },
|
||||
};
|
||||
|
||||
// 积分兑换明细的手风琴配置:默认收起,标题行显示积分与时长摘要。
|
||||
const vipPointsGroupOptions = {
|
||||
collapsible: true,
|
||||
defaultCollapsed: true,
|
||||
listPreview: { prefix: '积分:', titleKey: 'points', descKey: 'time', descUnitKey: 'unit' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 会员商品字段集(参考主题 CFS_Module::vip_product)。
|
||||
*/
|
||||
function vipProductFields() {
|
||||
return [
|
||||
{ key: 'price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '699.00', min: 0, suffix: '元', help: '会员的实际购买价格' },
|
||||
{ key: 'show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '999.00', min: 0, suffix: '元', help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { key: 'tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||
{ key: 'time', label: '会员有效时间', type: 'spinner', default: 3, min: 0, max: 3600, step: 1, help: '开通会员的时长。填0则为永久会员' },
|
||||
{ key: 'unit', label: '时长单位', type: 'radio', default: 'month', inline: true, dependsOn: { key: 'time', operator: 'truthy' }, options: vipUnitOptions },
|
||||
{ key: '_time_permanent_hint', label: '', type: 'alert', alertType: 'success', ui_only: true, dependsOn: { key: 'time', operator: 'eq', value: 0 }, content: '会员有效时间已设置为:永久会员' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级 Tab 字段集(参考主题 CFS_Module::vip_tab,显示名称并入 Tab)。
|
||||
*/
|
||||
function vipLevelTabFields(level) {
|
||||
const dep = { key: `pay_user_vip_${level}_s`, value: true };
|
||||
return [
|
||||
{ key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', sanitize_type: 'html', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '支持HTML,请注意控制长度' },
|
||||
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', sanitize_type: 'html', default: '尊享专属特权', dependsOn: dep, help: '支持HTML' },
|
||||
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '支持安全HTML,建议使用 <li> 列表项' },
|
||||
{ key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' },
|
||||
{
|
||||
key: `vip_${level}_product`,
|
||||
label: '会员商品',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加会员商品',
|
||||
default: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
...vipProductGroupOptions,
|
||||
dependsOn: { all: [dep, { key: `vip_${level}_product_s`, value: true }] },
|
||||
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
|
||||
];
|
||||
}
|
||||
|
||||
export const vipSchema = {
|
||||
key: 'payment.vip',
|
||||
title: 'VIP会员',
|
||||
icon: 'fa fas fa-gem',
|
||||
description: '双级会员:等级名称、权益(下载次数与限速)、套餐、续费/升级与积分兑换。',
|
||||
description: '双级会员:等级开关、权益(下载次数与限速)、会员商品、续费/升级与积分兑换。',
|
||||
defaults: {
|
||||
nav_pay_vip: true,
|
||||
nav_user_pay_vip: true,
|
||||
pay_user_vip_1_s: true,
|
||||
pay_user_vip_1_name: '一级会员',
|
||||
pay_user_vip_1_desc: '尊享一级会员特权',
|
||||
pay_user_vip_1_equity: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>',
|
||||
pay_user_vip_1_name: '黄金会员',
|
||||
pay_user_vip_1_desc: '尊享专属特权',
|
||||
pay_user_vip_1_equity: VIP_EQUITY_DEFAULT,
|
||||
vip_1_product_s: true,
|
||||
vip_1_product: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_1_img_icon: '',
|
||||
pay_user_vip_2_s: true,
|
||||
pay_user_vip_2_name: '二级会员',
|
||||
pay_user_vip_2_desc: '尊享二级会员特权',
|
||||
pay_user_vip_2_equity: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>',
|
||||
pay_user_vip_2_name: '钻石会员',
|
||||
pay_user_vip_2_desc: '尊享专属特权',
|
||||
pay_user_vip_2_equity: VIP_EQUITY_DEFAULT,
|
||||
vip_2_product_s: true,
|
||||
vip_2_product: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_img_icon: '',
|
||||
pay_user_vip_desc: '',
|
||||
pay_user_vip_more: '',
|
||||
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||
pay_download_limit: 0,
|
||||
pay_download_speed: 0,
|
||||
pay_download_limit_vip_1: 0,
|
||||
@@ -29,19 +129,45 @@ export const vipSchema = {
|
||||
pay_download_limit_vip_2: 0,
|
||||
pay_download_speed_vip_2: 0,
|
||||
vip_1_product_s: true,
|
||||
payment_vip_1_products: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
||||
vip_2_product_s: true,
|
||||
payment_vip_2_products: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
||||
vip_renew: true,
|
||||
vip_renew_desc: '',
|
||||
vip_renew_desc: '立即续费会员,畅享VIP权益',
|
||||
vip_renew_price_type: 'discount',
|
||||
vip_renew_discount: 8,
|
||||
vip_renew_reduce: '20.00',
|
||||
vip_1_renew_product: [
|
||||
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_renew_product: [
|
||||
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_product_s: true,
|
||||
vip_renew: true,
|
||||
vip_renew_discount: 8,
|
||||
vip_renew_reduce: '20.00',
|
||||
vip_upgrade: true,
|
||||
vip_upgrade_desc: '',
|
||||
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
vip_upgrade_desc: '升级VIP会员,享更多会员权益',
|
||||
vip_upgrade_product: {
|
||||
month_to_month_s: true,
|
||||
unit_price: '10.00',
|
||||
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||
unit_tag_class: 'jb-yellow',
|
||||
jump_s: false,
|
||||
jump_price: '199.00',
|
||||
jump_show_price: '299.00',
|
||||
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
jump_tag_class: 'jb-red',
|
||||
permanent_price: '199.00',
|
||||
permanent_show_price: '299.00',
|
||||
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
permanent_tag_class: 'jb-red',
|
||||
},
|
||||
pay_vip_points_exchange_s: false,
|
||||
payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
pay_vip_points_exchange_product: [
|
||||
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||
{ points: 5000, level: 1, time: 'Permanent' },
|
||||
],
|
||||
pay_vip_points_exchange_desc: '',
|
||||
},
|
||||
groups: [
|
||||
@@ -67,17 +193,16 @@ export const vipSchema = {
|
||||
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
||||
{ key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' },
|
||||
{ key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true },
|
||||
{ key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '一级会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{ key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享一级会员特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{ key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', default: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用 <li> 列表项' },
|
||||
{ key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' },
|
||||
{
|
||||
key: 'vip_level_order_notice',
|
||||
label: '',
|
||||
type: 'notice',
|
||||
ui_only: true,
|
||||
alertType: 'danger',
|
||||
dependsOn: { all: [{ key: 'pay_user_vip_2_s', value: true }, { key: 'pay_user_vip_1_s', operator: 'falsy' }] },
|
||||
content: '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>',
|
||||
},
|
||||
{ key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true },
|
||||
{ key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '二级会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享二级会员特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', default: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '留空使用内置 vip-2.svg 图标' },
|
||||
{ key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' },
|
||||
{ key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -101,6 +226,8 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip1',
|
||||
title: '一级会员',
|
||||
titleKey: 'pay_user_vip_1_name',
|
||||
titleFallback: '一级会员',
|
||||
fields: [
|
||||
{ key: 'pay_download_limit_vip_1', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '一级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||
{ key: 'pay_download_speed_vip_1', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||
@@ -109,6 +236,8 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip2',
|
||||
title: '二级会员',
|
||||
titleKey: 'pay_user_vip_2_name',
|
||||
titleFallback: '二级会员',
|
||||
fields: [
|
||||
{ key: 'pay_download_limit_vip_2', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '二级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||
{ key: 'pay_download_speed_vip_2', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||
@@ -131,53 +260,111 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip1',
|
||||
title: '一级会员',
|
||||
titleKey: 'pay_user_vip_1_name',
|
||||
titleFallback: '一级会员',
|
||||
fields: [
|
||||
{ key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{
|
||||
key: 'payment_vip_1_products',
|
||||
label: '一级会员套餐',
|
||||
type: 'textarea',
|
||||
default: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
||||
dependsOn: { key: 'vip_1_product_s', value: true },
|
||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
||||
},
|
||||
...vipLevelTabFields(1),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'vip2',
|
||||
title: '二级会员',
|
||||
titleKey: 'pay_user_vip_2_name',
|
||||
titleFallback: '二级会员',
|
||||
fields: [
|
||||
{ key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{
|
||||
key: 'payment_vip_2_products',
|
||||
label: '二级会员套餐',
|
||||
type: 'textarea',
|
||||
default: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
||||
dependsOn: { key: 'vip_2_product_s', value: true },
|
||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
||||
},
|
||||
...vipLevelTabFields(2),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'renew',
|
||||
title: '会员续费',
|
||||
fields: [
|
||||
{ key: 'vip_renew', label: '允许续费', type: 'switch', default: true, help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费' },
|
||||
{ key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } },
|
||||
{
|
||||
key: 'vip_renew',
|
||||
label: '允许续费',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_desc',
|
||||
label: '续费介绍',
|
||||
type: 'input',
|
||||
sanitize_type: 'html',
|
||||
default: '立即续费会员,畅享VIP权益',
|
||||
dependsOn: { key: 'vip_renew', value: true },
|
||||
help: '续费会员的一句话简介,支持HTML',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_price_type',
|
||||
label: '续费价格类型',
|
||||
type: 'radio',
|
||||
type: 'select',
|
||||
default: 'discount',
|
||||
dependsOn: { key: 'vip_renew', value: true },
|
||||
options: [
|
||||
{ label: '相同价格', value: 'same' },
|
||||
{ label: '折扣价格', value: 'discount' },
|
||||
{ label: '优惠立减', value: 'reduce' },
|
||||
{ label: '自定义商品', value: 'customize' },
|
||||
],
|
||||
help: '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_discount',
|
||||
label: '续费折扣',
|
||||
type: 'slider',
|
||||
sanitize_type: 'float',
|
||||
default: 8,
|
||||
min: 0.1,
|
||||
max: 9.9,
|
||||
step: 0.1,
|
||||
unit: '折',
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'discount' },
|
||||
help: '在会员商品基础上打几折,如 8 = 八折',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_reduce',
|
||||
label: '续费减免金额',
|
||||
type: 'spinner',
|
||||
sanitize_type: 'money_decimal',
|
||||
default: '20.00',
|
||||
min: 0,
|
||||
max: 500,
|
||||
step: 5,
|
||||
unit: '元',
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'reduce' },
|
||||
help: '在会员商品基础上优惠的金额,优惠后总金额不能为0元',
|
||||
},
|
||||
{
|
||||
key: 'vip_1_renew_product',
|
||||
label: '一级会员续费',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加续费商品',
|
||||
default: [
|
||||
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||
help: '一级会员续费的商品选项',
|
||||
...vipProductGroupOptions,
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{
|
||||
key: 'vip_2_renew_product',
|
||||
label: '二级会员续费',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加续费商品',
|
||||
default: [
|
||||
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||
help: '二级会员续费的商品选项',
|
||||
...vipProductGroupOptions,
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{ key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } },
|
||||
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'input', sanitize_type: 'money_decimal', default: '20.00', min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -185,14 +372,50 @@ export const vipSchema = {
|
||||
title: '会员升级',
|
||||
fields: [
|
||||
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
|
||||
{ key: 'vip_upgrade_desc', label: '升级说明', type: 'textarea', default: '', dependsOn: { key: 'vip_upgrade', value: true } },
|
||||
{ key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', sanitize_type: 'html', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介,支持HTML' },
|
||||
{
|
||||
key: 'vip_upgrade_product',
|
||||
label: '升级产品',
|
||||
type: 'textarea',
|
||||
default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
label: '升级价格',
|
||||
type: 'accordion',
|
||||
default: {
|
||||
month_to_month_s: true,
|
||||
unit_price: '10.00',
|
||||
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||
unit_tag_class: 'jb-yellow',
|
||||
jump_s: false,
|
||||
jump_price: '199.00',
|
||||
jump_show_price: '299.00',
|
||||
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
jump_tag_class: 'jb-red',
|
||||
permanent_price: '199.00',
|
||||
permanent_show_price: '299.00',
|
||||
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
permanent_tag_class: 'jb-red',
|
||||
},
|
||||
dependsOn: { key: 'vip_upgrade', value: true },
|
||||
help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题',
|
||||
help: '会员升级的商品选项',
|
||||
sections: [
|
||||
{ title: '月费会员升级月费会员', keys: ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class'] },
|
||||
{ title: '月费会员升级永久会员', keys: ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class'] },
|
||||
{ title: '永久会员升级永久会员', keys: ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class'] },
|
||||
],
|
||||
fields: [
|
||||
{ key: '_upgrade_unit_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价' },
|
||||
{ key: 'month_to_month_s', label: '月费会员升级月费会员', type: 'switch', default: true },
|
||||
{ key: 'unit_price', label: '天单价', type: 'number', sanitize_type: 'money_decimal', default: '10.00', min: 0, unit: '元', dependsOn: { key: 'month_to_month_s', value: true } },
|
||||
{ key: 'unit_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', rows: 1, dependsOn: { key: 'month_to_month_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'unit_tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { all: [{ key: 'month_to_month_s', value: true }, { key: 'unit_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||
{ key: '_upgrade_jump_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项' },
|
||||
{ key: 'jump_s', label: '月费会员升级永久会员', type: 'switch', default: false },
|
||||
{ key: 'jump_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '月费会员升级永久会员的价格' },
|
||||
{ key: 'jump_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'jump_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, dependsOn: { key: 'jump_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'jump_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { all: [{ key: 'jump_s', value: true }, { key: 'jump_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||
{ key: 'permanent_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', help: '永久会员升级永久会员的价格' },
|
||||
{ key: 'permanent_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'permanent_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'permanent_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { key: 'permanent_tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -205,16 +428,72 @@ export const vipSchema = {
|
||||
title: '积分兑换会员',
|
||||
icon: 'fa fas fa-database',
|
||||
fields: [
|
||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false },
|
||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false, help: '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费' },
|
||||
{
|
||||
key: 'payment_vip_points_packages',
|
||||
label: '兑换套餐',
|
||||
type: 'textarea',
|
||||
default: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
key: 'pay_vip_points_exchange_product',
|
||||
label: '积分兑换明细',
|
||||
type: 'group',
|
||||
max: 16,
|
||||
button_title: '添加积分兑换商品',
|
||||
default: [
|
||||
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||
{ points: 5000, level: 1, time: 'Permanent' },
|
||||
],
|
||||
dependsOn: { key: 'pay_vip_points_exchange_s', value: true },
|
||||
help: '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)',
|
||||
help: '每一行为一个可兑换的会员套餐',
|
||||
...vipPointsGroupOptions,
|
||||
fields: [
|
||||
{ key: 'points', label: '使用积分', type: 'number', default: 100, min: 0, unit: '积分' },
|
||||
{
|
||||
key: 'level',
|
||||
label: '兑换会员',
|
||||
type: 'radio',
|
||||
default: 1,
|
||||
inline: true,
|
||||
options: [
|
||||
{ label: '一级会员', value: 1 },
|
||||
{ label: '二级会员', value: 2 },
|
||||
],
|
||||
},
|
||||
{ key: 'time', label: '兑换时长', type: 'input', default: '', help: '填 Permanent 为永久' },
|
||||
{
|
||||
key: 'unit',
|
||||
label: '时长单位',
|
||||
type: 'radio',
|
||||
default: 'day',
|
||||
inline: true,
|
||||
dependsOn: { key: 'time', operator: 'neq', value: 'Permanent' },
|
||||
options: vipUnitOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_points_exchange_s', value: true }, help: '显示在积分兑换弹窗底部,可以为提醒事项、兑换协议等,支持HTML代码' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'modal-copy',
|
||||
title: '界面文案',
|
||||
icon: 'fa fas fa-align-left',
|
||||
fields: [
|
||||
{
|
||||
key: 'pay_user_vip_desc',
|
||||
label: '会员介绍',
|
||||
type: 'input',
|
||||
sanitize_type: 'html',
|
||||
default: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||
help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍,支持HTML',
|
||||
},
|
||||
{
|
||||
key: 'pay_user_vip_more',
|
||||
label: '开通会员更多内容',
|
||||
type: 'textarea',
|
||||
sanitize_type: 'html',
|
||||
default: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||
rows: 4,
|
||||
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||
help: '显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码',
|
||||
},
|
||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -85,6 +85,8 @@ export const permissionsSchemas = {
|
||||
{ label: '每日签到', value: 'signin' },
|
||||
{ label: '前台投稿', value: 'front_submit' },
|
||||
{ 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 { levelSchema } from './level.js'
|
||||
import { badgeSchema } from './badge.js'
|
||||
import { wechatSchema } from './wechat.js'
|
||||
import { reportSchema } from './report.js'
|
||||
import { smsSchema } from './sms.js'
|
||||
|
||||
@@ -23,7 +22,6 @@ export const usersSchemas = {
|
||||
verify: verifySchema,
|
||||
level: levelSchema,
|
||||
badge: badgeSchema,
|
||||
wechat: wechatSchema,
|
||||
report: reportSchema,
|
||||
sms: smsSchema,
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
export const wechatSchema = {
|
||||
key: 'users.wechat',
|
||||
title: '微信通知',
|
||||
icon: 'fa fab fa-weixin',
|
||||
description: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
||||
defaults: {
|
||||
wechat_notify_enabled: false,
|
||||
wechat_app_id: '',
|
||||
wechat_app_secret: '',
|
||||
wechat_template_id: '',
|
||||
wechat_token: '',
|
||||
wechat_encoding_aes_key: '',
|
||||
wechat_notify_comment: true,
|
||||
wechat_notify_reply: true,
|
||||
wechat_notify_system: true,
|
||||
wechat_bind_entry: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'account',
|
||||
title: '公众号配置',
|
||||
icon: 'fa fab fa-weixin',
|
||||
fields: [
|
||||
{ key: 'wechat_notify_enabled', label: '启用微信通知', type: 'switch', default: false, help: '开启后用户可绑定微信接收通知' },
|
||||
{ key: 'wechat_app_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
||||
{ key: 'wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
||||
{ key: 'wechat_template_id', label: '模板消息 ID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在微信公众平台申请的模板消息 ID' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'verify',
|
||||
title: '服务器验证',
|
||||
icon: 'fa fas fa-check-circle',
|
||||
fields: [
|
||||
{ key: 'wechat_token', label: 'Token', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '微信公众平台服务器配置中的 Token' },
|
||||
{ key: 'wechat_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'triggers',
|
||||
title: '通知触发器',
|
||||
icon: 'fa fas fa-bell',
|
||||
fields: [
|
||||
{ key: 'wechat_notify_comment', label: '新评论通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
||||
{ key: 'wechat_notify_reply', label: '评论回复通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
||||
{ key: 'wechat_notify_system', label: '系统通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
||||
{ key: 'wechat_bind_entry', label: '显示绑定入口', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在用户中心显示微信绑定/解绑按钮' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -709,8 +709,9 @@
|
||||
color: var(--color-primary, #f04494);
|
||||
}
|
||||
|
||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but {
|
||||
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but,
|
||||
.widget a.img-badge {
|
||||
color: var(--this-color, #fff);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
|
||||
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
|
||||
+237
-359
@@ -916,6 +916,7 @@ strong, b {
|
||||
|
||||
/* === Full width === */
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -961,6 +962,8 @@ strong, b {
|
||||
.but > .fa:not(.fa-angle-right),
|
||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||
|
||||
.but.block { display: block; }
|
||||
|
||||
/* hover / active */
|
||||
.but:focus,
|
||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||
@@ -1092,7 +1095,7 @@ strong, b {
|
||||
|
||||
/* gradient modifiers (legacy .jb-*) */
|
||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
|
||||
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||
@@ -2062,6 +2065,22 @@ html.dark .alert--info { color: #93C5FD; }
|
||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||
|
||||
/* ============================================================
|
||||
* 8b. 头像形状统一 — 圆形基底
|
||||
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||
* ============================================================ */
|
||||
img.avatar,
|
||||
img.avatar-mini,
|
||||
img.avatar-md,
|
||||
img.avatar-lg,
|
||||
.avatar-img img {
|
||||
border-radius: var(--radius-avatar, 50%);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||
@@ -2638,7 +2657,8 @@ main.container,
|
||||
.user-card.author .user-name > name {
|
||||
overflow: visible;
|
||||
}
|
||||
.user-card.author .user-name > name > .display-name {
|
||||
.user-card.author .user-name > name > .display-name,
|
||||
.user-card.widget .user-name > name > .display-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -2688,31 +2708,35 @@ main.container,
|
||||
/**
|
||||
* Header account popover.
|
||||
*
|
||||
* The shared legacy navigation styles provide the popover shell. This module
|
||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
||||
* The shared legacy navigation styles provide the popover shell, and every
|
||||
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||
* through the reference legacy classes. This module only owns the navigation
|
||||
* shell anchoring plus the account action-row geometry.
|
||||
*/
|
||||
|
||||
/* The desktop action cluster owns one horizontal alignment context. */
|
||||
.navbar-top .aksrt-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 50px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions > *,
|
||||
.navbar-top .aksrt-navbar-account,
|
||||
.navbar-top .aksrt-navbar-account > li {
|
||||
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||
* override only zeroes the horizontal margins, so every right-cluster group
|
||||
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||
* its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
|
||||
* min-height: it would double-count the 8px and push that group's content 8px
|
||||
* below the line while inflating the navbar from 50px to 58px. */
|
||||
.navbar-top .aksrt-navbar-account {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account > li {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -2725,39 +2749,6 @@ main.container,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account {
|
||||
gap: 4px;
|
||||
min-height: 50px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 29px;
|
||||
margin: 0;
|
||||
padding: .3em 1em !important;
|
||||
border-radius: var(--radius-btn, 4px);
|
||||
font-size: .8em;
|
||||
line-height: 1.44;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-nav-icon,
|
||||
.navbar-top .aksrt-nav-avatar {
|
||||
display: inline-flex !important;
|
||||
@@ -2790,33 +2781,34 @@ main.container,
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
||||
/* The account-row chips, the theme and the language toggles all want the 29px
|
||||
* midline circle. The popover's quick-login buttons are .toggle-radius too and
|
||||
* also live inside .navbar-form, but they must keep the .social_loginbar
|
||||
* geometry from main.css: in a flex container flex-basis outranks width, so
|
||||
* without this exemption the buttons collapse to 29px and lose the 5px side
|
||||
* margins that space them apart. */
|
||||
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
|
||||
flex: 0 0 29px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-language {
|
||||
/* Match the account row's 34px cross-size so the 29px toggle shares the 25px
|
||||
* midline instead of sitting 2.5px above it. */
|
||||
.navbar-top .navbar-form.flex.ac {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
||||
* the absolute panel's static position beside the compact CTA. */
|
||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
||||
top: 100%;
|
||||
right: 0;
|
||||
left: auto;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 24px);
|
||||
margin: 0;
|
||||
z-index: 1002;
|
||||
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||
* would move that static position to the flex container's cross-start. */
|
||||
.navbar-top .navbar-but > .hover-show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||
@@ -2852,85 +2844,41 @@ main.container,
|
||||
}
|
||||
|
||||
.aksrt-header-userbox {
|
||||
width: 260px;
|
||||
color: var(--color-ink-2);
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info {
|
||||
min-height: 44px;
|
||||
padding: 2px;
|
||||
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||
.aksrt-header-userbox .muted-box.padding-6 {
|
||||
background-color: var(--this-bg, var(--color-line-soft));
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info > .avatar {
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||
* declarations are the last word in the cascade. Re-assert them here, which
|
||||
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||
.payvip-icon {
|
||||
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||
color: #ece0e0;
|
||||
--this-color: #ece0e0;
|
||||
border: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-right,
|
||||
.aksrt-header-userbox .user-right > .flex1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-right b {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .display-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-right .img-icon,
|
||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
||||
flex: 0 0 auto;
|
||||
height: 1.1em;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info > .abs-right {
|
||||
top: 50%;
|
||||
right: 2px;
|
||||
transform: translateY(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-2);
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .aksrt-notification-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border: 2px solid var(--surface-1);
|
||||
border-radius: 999px;
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||
.aksrt-nav-notification .aksrt-notification-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
@@ -2950,222 +2898,20 @@ main.container,
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aksrt-header-wallet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-2);
|
||||
color: var(--color-ink-2);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.aksrt-header-wallet:hover {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-header-wallet__item {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-wallet__item i {
|
||||
margin-right: 2px;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.aksrt-header-wallet__item b {
|
||||
margin-left: 2px;
|
||||
color: var(--color-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aksrt-header-checkin {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100% !important;
|
||||
min-height: 36px;
|
||||
margin: 0;
|
||||
padding: 8px 10px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
background: #fff6db !important;
|
||||
color: #8a5a00 !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-checkin > i {
|
||||
margin: 0 !important;
|
||||
color: #b78103 !important;
|
||||
}
|
||||
|
||||
.aksrt-header-checkin:hover {
|
||||
background: #fff0c2 !important;
|
||||
color: #8a5a00 !important;
|
||||
}
|
||||
|
||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
||||
.aksrt-header-checkin:disabled,
|
||||
.aksrt-header-checkin:disabled:hover {
|
||||
cursor: default;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__cta,
|
||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
||||
.aksrt-header-payvip:not(.is-vip) .but {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 6px !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 36px;
|
||||
margin: 0 !important;
|
||||
padding: 8px 10px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
||||
color: #fff !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__cta > i,
|
||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
||||
margin: 0 !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__cta:hover {
|
||||
color: #fff !important;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__active {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px !important;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 7px 10px !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__level {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__level > i {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__exp {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none !important;
|
||||
box-shadow: none;
|
||||
color: inherit;
|
||||
font-size: .85em;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
opacity: .9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||
* icon tiles and must opt out or their labels overlap the following row. */
|
||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
* icon tiles and must opt out or their labels overlap the following row.
|
||||
* The tile geometry itself stays with the shared legacy rules
|
||||
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||
* so the action row keeps the reference proportions. */
|
||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||
.aksrt-header-userbox .header-user-href > a {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-height: none;
|
||||
padding: 10px 2px 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href > a:hover {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
||||
flex: 0 0 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-navbar .aksrt-header-userbox {
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-checkin {
|
||||
background: rgba(255, 193, 7, .18) !important;
|
||||
color: #f0c14b !important;
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-checkin > i {
|
||||
color: #f0c14b !important;
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-wallet {
|
||||
background: rgba(255, 255, 255, .06);
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-wallet__item,
|
||||
body.dark-theme .aksrt-header-wallet__item b {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.aksrt-header-userbox .header-user-href {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
||||
flex-basis: 34px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* source: assets/css/frontend/card-primitives.css */
|
||||
@@ -6496,7 +6242,8 @@ body.aksrt-modal-runtime-open {
|
||||
底色走 --main-radius / --main-bg-color;移动端由 --bottom 底部抽屉接管。 */
|
||||
.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-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));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
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-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;
|
||||
}
|
||||
|
||||
@@ -6513,9 +6261,13 @@ body.aksrt-modal-runtime-open {
|
||||
.aksrt-after-sale-apply-modal .modal-colorful-header,
|
||||
.aksrt-after-sale-detail-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-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;
|
||||
overflow: hidden;
|
||||
color: var(--this-color);
|
||||
@@ -6525,7 +6277,9 @@ body.aksrt-modal-runtime-open {
|
||||
|
||||
.aksrt-after-sale-apply-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;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -6539,13 +6293,17 @@ body.aksrt-modal-runtime-open {
|
||||
|
||||
.aksrt-after-sale-apply-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;
|
||||
}
|
||||
|
||||
.aksrt-after-sale-apply-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;
|
||||
top: 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::after,
|
||||
.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::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;
|
||||
color: var(--this-color);
|
||||
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-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%;
|
||||
bottom: 0;
|
||||
width: 323px;
|
||||
@@ -6583,7 +6349,9 @@ body.aksrt-modal-runtime-open {
|
||||
|
||||
.aksrt-after-sale-apply-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;
|
||||
right: -558px;
|
||||
bottom: 0;
|
||||
@@ -6595,7 +6363,9 @@ body.aksrt-modal-runtime-open {
|
||||
|
||||
.aksrt-after-sale-apply-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;
|
||||
bottom: 0;
|
||||
left: -262px;
|
||||
@@ -6669,6 +6439,114 @@ body.aksrt-modal-runtime-open {
|
||||
.aksrt-after-sale-status.is-rejected,
|
||||
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
||||
|
||||
/* 售后处理记录时间线:[节点][处理状态][时间] 一行,处理备注另起一行并对齐状态文字。
|
||||
售后详细弹窗与订单详情弹窗的「售后记录」共用同一套标记;竖线为条目的
|
||||
::before,末项不收口,避免出现悬空的线段。 */
|
||||
.aksrt-after-sale-log { display: grid; gap: 6px; }
|
||||
.aksrt-after-sale-log__empty { color: var(--modal-muted); font-size: 12px; }
|
||||
.aksrt-after-sale-timeline { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||
.aksrt-after-sale-timeline__item { --as-accent: var(--modal-muted); position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; padding: 0 0 14px; }
|
||||
.aksrt-after-sale-timeline__item:last-child { padding-bottom: 0; }
|
||||
.aksrt-after-sale-timeline__item.is-pending { --as-accent: var(--modal-warning); }
|
||||
.aksrt-after-sale-timeline__item.is-processing { --as-accent: var(--modal-primary); }
|
||||
.aksrt-after-sale-timeline__item.is-completed { --as-accent: var(--modal-success); }
|
||||
.aksrt-after-sale-timeline__item.is-rejected,
|
||||
.aksrt-after-sale-timeline__item.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||
.aksrt-after-sale-timeline__item::before { content: ""; position: absolute; top: 14px; bottom: -2px; left: 4px; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--as-accent) 30%, var(--modal-line)); }
|
||||
.aksrt-after-sale-timeline__item:last-child::before { display: none; }
|
||||
.aksrt-after-sale-timeline__knot { position: relative; z-index: 1; width: 10px; height: 10px; margin-top: 4px; border: 2px solid var(--as-accent); border-radius: 50%; background: var(--modal-surface, #fff); }
|
||||
.aksrt-after-sale-timeline__body { display: grid; gap: 3px; min-width: 0; }
|
||||
.aksrt-after-sale-timeline__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
||||
.aksrt-after-sale-timeline__status { color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||
.aksrt-after-sale-timeline__time { color: var(--modal-muted); font-size: 11px; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-after-sale-timeline__note { margin: 0; color: var(--modal-ink); font-size: 12.5px; line-height: 1.65; word-break: break-word; }
|
||||
|
||||
/* 状态行右侧单元格:状态徽章 + 「撤销售后」入口(仅待审核工单渲染)。 */
|
||||
.aksrt-after-sale-detail__statusCell { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.aksrt-after-sale-withdraw { display: inline-flex; align-items: center; gap: 3px; padding: 0; border: 0; background: transparent; color: var(--modal-danger, #ef4444); font-size: 12px; font-weight: 600; line-height: 1.4; cursor: pointer; transition: color .15s ease; }
|
||||
.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 */
|
||||
/**
|
||||
* 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 === */
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -182,6 +183,8 @@
|
||||
.but > .fa:not(.fa-angle-right),
|
||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||
|
||||
.but.block { display: block; }
|
||||
|
||||
/* hover / active */
|
||||
.but:focus,
|
||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||
@@ -313,7 +316,7 @@
|
||||
|
||||
/* gradient modifiers (legacy .jb-*) */
|
||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
|
||||
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||
|
||||
@@ -424,6 +424,22 @@
|
||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||
|
||||
/* ============================================================
|
||||
* 8b. 头像形状统一 — 圆形基底
|
||||
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||
* ============================================================ */
|
||||
img.avatar,
|
||||
img.avatar-mini,
|
||||
img.avatar-md,
|
||||
img.avatar-lg,
|
||||
.avatar-img img {
|
||||
border-radius: var(--radius-avatar, 50%);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||
|
||||
@@ -242,7 +242,8 @@ main.container,
|
||||
.user-card.author .user-name > name {
|
||||
overflow: visible;
|
||||
}
|
||||
.user-card.author .user-name > name > .display-name {
|
||||
.user-card.author .user-name > name > .display-name,
|
||||
.user-card.widget .user-name > name > .display-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
/**
|
||||
* Header account popover.
|
||||
*
|
||||
* The shared legacy navigation styles provide the popover shell. This module
|
||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
||||
* The shared legacy navigation styles provide the popover shell, and every
|
||||
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||
* through the reference legacy classes. This module only owns the navigation
|
||||
* shell anchoring plus the account action-row geometry.
|
||||
*/
|
||||
|
||||
/* The desktop action cluster owns one horizontal alignment context. */
|
||||
.navbar-top .aksrt-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 50px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions > *,
|
||||
.navbar-top .aksrt-navbar-account,
|
||||
.navbar-top .aksrt-navbar-account > li {
|
||||
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||
* override only zeroes the horizontal margins, so every right-cluster group
|
||||
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||
* its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
|
||||
* min-height: it would double-count the 8px and push that group's content 8px
|
||||
* below the line while inflating the navbar from 50px to 58px. */
|
||||
.navbar-top .aksrt-navbar-account {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account > li {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -38,39 +42,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account {
|
||||
gap: 4px;
|
||||
min-height: 50px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 29px;
|
||||
margin: 0;
|
||||
padding: .3em 1em !important;
|
||||
border-radius: var(--radius-btn, 4px);
|
||||
font-size: .8em;
|
||||
line-height: 1.44;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-nav-icon,
|
||||
.navbar-top .aksrt-nav-avatar {
|
||||
display: inline-flex !important;
|
||||
@@ -103,33 +74,34 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
||||
/* The account-row chips, the theme and the language toggles all want the 29px
|
||||
* midline circle. The popover's quick-login buttons are .toggle-radius too and
|
||||
* also live inside .navbar-form, but they must keep the .social_loginbar
|
||||
* geometry from main.css: in a flex container flex-basis outranks width, so
|
||||
* without this exemption the buttons collapse to 29px and lose the 5px side
|
||||
* margins that space them apart. */
|
||||
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
|
||||
flex: 0 0 29px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-language {
|
||||
/* Match the account row's 34px cross-size so the 29px toggle shares the 25px
|
||||
* midline instead of sitting 2.5px above it. */
|
||||
.navbar-top .navbar-form.flex.ac {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
||||
* the absolute panel's static position beside the compact CTA. */
|
||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
||||
top: 100%;
|
||||
right: 0;
|
||||
left: auto;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 24px);
|
||||
margin: 0;
|
||||
z-index: 1002;
|
||||
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||
* would move that static position to the flex container's cross-start. */
|
||||
.navbar-top .navbar-but > .hover-show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||
@@ -165,85 +137,41 @@
|
||||
}
|
||||
|
||||
.aksrt-header-userbox {
|
||||
width: 260px;
|
||||
color: var(--color-ink-2);
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info {
|
||||
min-height: 44px;
|
||||
padding: 2px;
|
||||
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||
.aksrt-header-userbox .muted-box.padding-6 {
|
||||
background-color: var(--this-bg, var(--color-line-soft));
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info > .avatar {
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||
* declarations are the last word in the cascade. Re-assert them here, which
|
||||
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||
.payvip-icon {
|
||||
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||
color: #ece0e0;
|
||||
--this-color: #ece0e0;
|
||||
border: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-right,
|
||||
.aksrt-header-userbox .user-right > .flex1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-right b {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .display-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-right .img-icon,
|
||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
||||
flex: 0 0 auto;
|
||||
height: 1.1em;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info > .abs-right {
|
||||
top: 50%;
|
||||
right: 2px;
|
||||
transform: translateY(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-2);
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .aksrt-notification-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border: 2px solid var(--surface-1);
|
||||
border-radius: 999px;
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||
.aksrt-nav-notification .aksrt-notification-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
@@ -263,220 +191,18 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aksrt-header-wallet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-2);
|
||||
color: var(--color-ink-2);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.aksrt-header-wallet:hover {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-header-wallet__item {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-wallet__item i {
|
||||
margin-right: 2px;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.aksrt-header-wallet__item b {
|
||||
margin-left: 2px;
|
||||
color: var(--color-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aksrt-header-checkin {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100% !important;
|
||||
min-height: 36px;
|
||||
margin: 0;
|
||||
padding: 8px 10px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
background: #fff6db !important;
|
||||
color: #8a5a00 !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-checkin > i {
|
||||
margin: 0 !important;
|
||||
color: #b78103 !important;
|
||||
}
|
||||
|
||||
.aksrt-header-checkin:hover {
|
||||
background: #fff0c2 !important;
|
||||
color: #8a5a00 !important;
|
||||
}
|
||||
|
||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
||||
.aksrt-header-checkin:disabled,
|
||||
.aksrt-header-checkin:disabled:hover {
|
||||
cursor: default;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__cta,
|
||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
||||
.aksrt-header-payvip:not(.is-vip) .but {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 6px !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 36px;
|
||||
margin: 0 !important;
|
||||
padding: 8px 10px !important;
|
||||
border: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
||||
color: #fff !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__cta > i,
|
||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
||||
margin: 0 !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__cta:hover {
|
||||
color: #fff !important;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__active {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px !important;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 7px 10px !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__level {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__level > i {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.aksrt-header-payvip__exp {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none !important;
|
||||
box-shadow: none;
|
||||
color: inherit;
|
||||
font-size: .85em;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
opacity: .9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||
* icon tiles and must opt out or their labels overlap the following row. */
|
||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
* icon tiles and must opt out or their labels overlap the following row.
|
||||
* The tile geometry itself stays with the shared legacy rules
|
||||
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||
* so the action row keeps the reference proportions. */
|
||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||
.aksrt-header-userbox .header-user-href > a {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-height: none;
|
||||
padding: 10px 2px 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href > a:hover {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
||||
flex: 0 0 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-navbar .aksrt-header-userbox {
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-checkin {
|
||||
background: rgba(255, 193, 7, .18) !important;
|
||||
color: #f0c14b !important;
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-checkin > i {
|
||||
color: #f0c14b !important;
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-wallet {
|
||||
background: rgba(255, 255, 255, .06);
|
||||
}
|
||||
|
||||
body.dark-theme .aksrt-header-wallet__item,
|
||||
body.dark-theme .aksrt-header-wallet__item b {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.aksrt-header-userbox .header-user-href {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
||||
flex-basis: 34px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
margin: auto;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 社交登录分隔线 */
|
||||
.social-login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 24px 0 16px;
|
||||
color: var(--color-ink-3, #94a3b8);
|
||||
font-size: 13px;
|
||||
}
|
||||
.social-login-divider::before,
|
||||
.social-login-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--color-line, #e2e8f0);
|
||||
}
|
||||
.social-login-divider span {
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oauth-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.oauth-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-btn);
|
||||
border: 1px solid var(--color-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-ink-3);
|
||||
transition: border-color 0.2s, color 0.2s;
|
||||
}
|
||||
.oauth-btn i {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.oauth-btn:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.auth-footer-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
|
||||
@@ -50,8 +50,9 @@
|
||||
color: var(--color-primary, #f04494);
|
||||
}
|
||||
|
||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but {
|
||||
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but,
|
||||
.widget a.img-badge {
|
||||
color: var(--this-color, #fff);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,27 @@
|
||||
z-index: 100055;
|
||||
}
|
||||
|
||||
/* public.css redeclares .muted-box after legacy utilities, overriding its
|
||||
background, radius, and padding utilities. Restore the reference box contract. */
|
||||
.modal[data-aksrt-vip-points-modal] .muted-box.padding-h10 {
|
||||
background-color: var(--this-bg, var(--muted-border-color));
|
||||
border-radius: var(--main-radius);
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.modal[data-aksrt-vip-points-modal] .muted-box.padding-10,
|
||||
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||
background-color: var(--this-bg, var(--muted-border-color));
|
||||
border-radius: var(--main-radius);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
color: var(--muted-2-color, #999);
|
||||
}
|
||||
|
||||
.pay-vip-form-row {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -102,21 +123,85 @@
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.vipbg-v2 {
|
||||
/* 会员弹窗骨架屏(pay-vip.js vipSkeleton()):仿真实弹窗布局——
|
||||
用户框 + 等级卡行 + 双列套餐面板。线条/色块与 shimmer 动画复用全局
|
||||
.aksrt-modal-runtime__skeleton-line/-block 与 @keyframes
|
||||
aksrt-modal-skeleton(build/public.min.css 已全局入队);
|
||||
这里只定义 VIP 专属形状(头像圆、tab 卡、双列 grid、按钮条),
|
||||
与 .pay-vip-form-row 的 640px 双列阈值保持一致。 */
|
||||
.aksrt-vip-modal-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__tab {
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__pane {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__products {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__button {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/* 专属形状共用骨架底色与 shimmer(值与 modal-runtime.css 的线条/块一致)。 */
|
||||
.aksrt-vip-modal-skeleton__avatar,
|
||||
.aksrt-vip-modal-skeleton__tab,
|
||||
.aksrt-vip-modal-skeleton__button {
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ffb347, #ff6b6b);
|
||||
background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16));
|
||||
background-size: 200% 100%;
|
||||
animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.vipbg-v2.level-2 {
|
||||
background: linear-gradient(90deg, #9b59b6, #3f51b5);
|
||||
}
|
||||
|
||||
.vip-expdate-tag {
|
||||
font-size: 12px;
|
||||
@media (min-width: 640px) {
|
||||
.aksrt-vip-modal-skeleton__pane {
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
}
|
||||
}
|
||||
+9
-14
@@ -5936,7 +5936,7 @@ body:has(.modal.in) .float-btn,
|
||||
}
|
||||
|
||||
.payvip-icon:before {
|
||||
content: '\f219';
|
||||
content: '\f3a5';
|
||||
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
||||
font-size: 3.5em;
|
||||
right: -0.2em !important;
|
||||
@@ -5944,16 +5944,6 @@ body:has(.modal.in) .float-btn,
|
||||
opacity: 0.06;
|
||||
}
|
||||
|
||||
.vipbg-v1 {
|
||||
background: linear-gradient(60deg, #f0bf81 0, #fdefd6 30%, #f2deb9 40%, #fdc277 80%, #f9e1b7 100%);
|
||||
color: #8c5728;
|
||||
--this-color: #8c5728;
|
||||
}
|
||||
|
||||
.vipbg-v1:before {
|
||||
content: '\f006';
|
||||
}
|
||||
|
||||
.vip-texticon {
|
||||
font-size: 0.85em;
|
||||
background: transparent;
|
||||
@@ -9665,11 +9655,16 @@ img.alignleft {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap
|
||||
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高;
|
||||
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */
|
||||
/* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
|
||||
* 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
|
||||
* "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
|
||||
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
|
||||
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
|
||||
.user-card .card-content .img-badge {
|
||||
line-height: 1.42857143;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: auto;
|
||||
text-rendering: auto;
|
||||
}
|
||||
|
||||
.user-card name .display-name {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
|
||||
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
|
||||
@@ -34,7 +34,7 @@
|
||||
.friend-card__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-avatar, 50%);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface-2);
|
||||
|
||||
+215
-204
@@ -1,204 +1,215 @@
|
||||
/* Knowledge-payment order center */
|
||||
.aksrt-orders-page {
|
||||
--orders-ink: var(--color-ink, #1d2738);
|
||||
--orders-muted: var(--color-ink-3, #758096);
|
||||
--orders-line: color-mix(in srgb, var(--color-line, #e1e6ef) 88%, transparent);
|
||||
padding: 0;
|
||||
color: var(--orders-ink);
|
||||
}
|
||||
|
||||
.uc-orders-panel__head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(285px, 42%);
|
||||
gap: 28px;
|
||||
align-items: end;
|
||||
padding: 4px 0 22px;
|
||||
border-bottom: 1px solid var(--orders-line);
|
||||
}
|
||||
|
||||
.aksrt-orders-page__eyebrow {
|
||||
margin: 0 0 7px;
|
||||
color: var(--color-primary, #3b82f6);
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.uc-orders-panel__title { display: flex; align-items: center; gap: 9px; font-size: 21px; }
|
||||
.uc-orders-panel__title-icon { color: var(--color-primary, #3b82f6); font-size: 17px; }
|
||||
.uc-orders-panel__title-wrap > p:last-child { margin-bottom: 0; }
|
||||
|
||||
.aksrt-orders-page__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
border: 1px solid var(--orders-line);
|
||||
border-radius: 14px;
|
||||
background: var(--surface-1, #fff);
|
||||
overflow: hidden;
|
||||
}
|
||||
.aksrt-orders-page__stat { display: grid; gap: 5px; padding: 13px 15px; border-right: 1px solid var(--orders-line); }
|
||||
.aksrt-orders-page__stat:last-child { border-right: 0; }
|
||||
.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; }
|
||||
.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); }
|
||||
.aksrt-orders-page__stat strong.is-paid { color: var(--color-success, #168560); }
|
||||
|
||||
.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); }
|
||||
.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; }
|
||||
.aksrt-orders-page__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--orders-ink); font-size: 13px; }
|
||||
.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; }
|
||||
.aksrt-orders-page .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); }
|
||||
.aksrt-orders-page .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); }
|
||||
.aksrt-orders-page .btn-ghost { background: transparent; color: var(--orders-muted); }
|
||||
.aksrt-orders-page__filter-toggle { background: var(--surface-2, #f4f6f9); color: var(--orders-ink); white-space: nowrap; }
|
||||
|
||||
.aksrt-orders-page__filter-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); }
|
||||
.aksrt-orders-page__filter-panel[hidden] { display: none !important; }
|
||||
.aksrt-orders-page__filter-group { display: flex; flex: 1 1 100%; gap: 10px; align-items: center; }
|
||||
.aksrt-orders-page__filter-label { flex: 0 0 58px; color: var(--orders-muted); font-size: 12px; font-weight: 650; }
|
||||
.aksrt-orders-page__status-filters, .aksrt-orders-page__type-filters { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.aksrt-orders-page__filter-chip { display: inline-flex; gap: 5px; align-items: center; padding: 5px 9px; border: 1px solid var(--orders-line); border-radius: 999px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; }
|
||||
.aksrt-orders-page__filter-chip.is-active { border-color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-1, #fff)); color: var(--color-primary, #3b82f6); font-weight: 700; }
|
||||
.aksrt-orders-page__filter-count { display: inline-grid; min-width: 17px; height: 17px; padding: 0 4px; place-items: center; border-radius: 999px; background: var(--color-warning, #c87819); color: #fff; font-size: 10px; line-height: 1; }
|
||||
.aksrt-orders-page__clear { margin-left: auto; padding: 0; border: 0; background: transparent; color: var(--color-primary, #3b82f6); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; }
|
||||
|
||||
.aksrt-orders-page__list { display: grid; gap: 10px; }
|
||||
.aksrt-orders-page__card { overflow: hidden; border: 1px solid var(--orders-line); border-radius: 13px; background: var(--surface-1, #fff); transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease; }
|
||||
.aksrt-orders-page__card:hover { border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 36%, var(--orders-line)); box-shadow: 0 8px 22px rgba(30, 48, 79, .07); transform: translateY(-1px); }
|
||||
.aksrt-orders-page__card-rail { display: flex; gap: 10px; align-items: center; padding: 9px 14px; background: var(--surface-2, #f7f8fb); color: var(--orders-muted); font-size: 11px; }
|
||||
.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-orders-page__card-status { font-weight: 700; }
|
||||
.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); }
|
||||
.aksrt-orders-page__card-status.is-paid { color: var(--color-success, #168560); }
|
||||
.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; }
|
||||
.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; }
|
||||
.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); }
|
||||
.aksrt-orders-page__card-copy { min-width: 0; }
|
||||
.aksrt-orders-page__card-copy h3 { display: -webkit-box; margin: 0 0 5px; overflow: hidden; color: var(--orders-ink); font-size: 14px; line-height: 1.35; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.aksrt-orders-page__card-copy p { margin: 0; color: var(--orders-muted); font-family: var(--font-mono, monospace); font-size: 11px; }
|
||||
.aksrt-orders-page__card-value { display: grid; gap: 3px; text-align: right; }
|
||||
.aksrt-orders-page__card-value strong { color: var(--orders-ink); font-size: 15px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-orders-page__card-value span { color: var(--orders-muted); font-size: 11px; }
|
||||
.aksrt-orders-page__card-actions { display: flex; gap: 7px; align-items: center; justify-content: flex-end; padding: 9px 14px; border-top: 1px solid var(--orders-line); }
|
||||
.aksrt-orders-page__countdown { margin-right: auto; color: var(--color-warning, #c87819); font-size: 11px; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.aksrt-orders-page__pagination { display: flex; justify-content: center; gap: 6px; margin-top: 20px; }
|
||||
.aksrt-orders-page__page-chip { display: grid; width: 31px; height: 31px; padding: 0; place-items: center; border: 1px solid var(--orders-line); border-radius: 8px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; }
|
||||
.aksrt-orders-page__page-chip.is-active { border-color: var(--color-primary, #3b82f6); background: var(--color-primary, #3b82f6); color: #fff; }
|
||||
.aksrt-orders-page [data-aksrt-order-list], .aksrt-orders-page__stats, .aksrt-orders-page__pagination { transition: opacity .16s ease; }
|
||||
.aksrt-orders-page.is-loading [data-aksrt-order-list], .aksrt-orders-page.is-loading .aksrt-orders-page__stats, .aksrt-orders-page.is-loading .aksrt-orders-page__pagination { opacity: .55; pointer-events: none; }
|
||||
.aksrt-orders-page__empty { padding: 52px 20px; border: 1px dashed var(--orders-line); border-radius: 13px; background: var(--surface-2, #f7f8fb); text-align: center; }
|
||||
.aksrt-orders-page__empty i { display: block; margin-bottom: 12px; color: var(--color-primary, #3b82f6); font-size: 30px; }
|
||||
.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; }
|
||||
|
||||
/* ---- Order detail modal (front-end order center) ---- */
|
||||
/* 弹窗 portal 到 <body>,读不到页面上的 `--orders-*` 别名,因此把本模块的局部
|
||||
别名映射到 modal-runtime.css 声明的共享 portal 基座 `--modal-*`;
|
||||
路由文件不应再自行推导调色板值。 */
|
||||
.aksrt-order-detail-modal {
|
||||
--detail-ink: var(--modal-ink);
|
||||
--detail-muted: var(--modal-muted);
|
||||
--detail-line: var(--modal-line);
|
||||
--detail-surface: var(--modal-surface);
|
||||
--detail-surface-2: var(--modal-surface-2);
|
||||
--detail-surface-3: var(--modal-surface-3);
|
||||
--detail-primary: var(--modal-primary);
|
||||
--detail-warning: var(--modal-warning);
|
||||
--detail-success: var(--modal-success);
|
||||
}
|
||||
|
||||
/* 订单详情弹窗:与售后弹窗统一为 modal-mini 几何(400px);标题区改用蓝色炫彩头,运行时不再渲染默认标题栏。 */
|
||||
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||
width: min(400px, calc(100vw - 32px));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
background: var(--main-bg-color, #fff);
|
||||
}
|
||||
.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; }
|
||||
|
||||
/* Status banner — a single local variable drives the whole colour scheme. */
|
||||
.aksrt-order-detail__hero { --hero-accent: var(--detail-primary); --hero-text: color-mix(in srgb, var(--hero-accent) 72%, var(--detail-ink)); display: flex; align-items: center; gap: 13px; padding: 15px; border: 1px solid color-mix(in srgb, var(--hero-accent) 20%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--hero-accent) 12%, var(--detail-surface)); }
|
||||
.aksrt-order-detail__hero.is-pending { --hero-accent: var(--detail-warning); }
|
||||
.aksrt-order-detail__hero.is-paid { --hero-accent: var(--detail-success); }
|
||||
.aksrt-order-detail__hero.is-refunded, .aksrt-order-detail__hero.is-closed { --hero-accent: var(--detail-muted); }
|
||||
.aksrt-order-detail__hero-icon { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; border-radius: 12px; background: color-mix(in srgb, var(--hero-accent) 16%, transparent); color: var(--hero-text); font-size: 19px; }
|
||||
.aksrt-order-detail__hero-copy { display: grid; flex: 1 1 auto; gap: 4px; min-width: 0; }
|
||||
.aksrt-order-detail__status { color: var(--hero-text); font-size: 15px; font-weight: 750; line-height: 1.2; }
|
||||
.aksrt-order-detail__desc { margin: 0; color: var(--detail-muted); font-size: 12px; line-height: 1.45; }
|
||||
.aksrt-order-detail__countdown { color: var(--hero-text); font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__hero-amount { display: grid; flex: 0 0 auto; gap: 3px; justify-items: end; text-align: right; }
|
||||
.aksrt-order-detail__hero-amount strong { color: var(--detail-ink); font-size: 21px; font-weight: 750; line-height: 1.1; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__hero-amount span { color: var(--detail-muted); font-size: 11px; }
|
||||
|
||||
.aksrt-order-detail__group { display: grid; gap: 10px; }
|
||||
.aksrt-order-detail__group-title { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--detail-ink); font-size: 13px; font-weight: 700; line-height: 1.3; }
|
||||
.aksrt-order-detail__group-title::before { content: ""; width: 3px; height: 13px; border-radius: 2px; background: var(--detail-primary); }
|
||||
|
||||
.aksrt-order-detail__product { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 12px; align-items: center; padding: 12px; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); }
|
||||
.aksrt-order-detail__thumb { width: 54px; height: 54px; border-radius: 10px; object-fit: cover; }
|
||||
.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--detail-surface-2); color: var(--detail-primary); font-size: 19px; }
|
||||
.aksrt-order-detail__product-copy { display: grid; gap: 6px; min-width: 0; }
|
||||
.aksrt-order-detail__product-title { display: -webkit-box; overflow: hidden; color: var(--detail-ink); font-size: 14px; line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.aksrt-order-detail__product-meta { display: inline-flex; justify-self: start; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--detail-primary) 9%, var(--detail-surface-2)); color: var(--detail-primary); font-size: 11px; font-weight: 650; line-height: 1.5; }
|
||||
|
||||
.aksrt-order-detail__list { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); }
|
||||
.aksrt-order-detail__list > div { display: grid; grid-template-columns: 84px minmax(0, 1fr); align-items: center; gap: 12px; padding: 11px 13px; border-bottom: 1px solid var(--detail-line); }
|
||||
.aksrt-order-detail__list > div:last-child { border-bottom: 0; }
|
||||
.aksrt-order-detail__list dt { color: var(--detail-muted); font-size: 12.5px; }
|
||||
.aksrt-order-detail__list dd { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; margin: 0; color: var(--detail-ink); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; }
|
||||
.aksrt-order-detail__value-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.aksrt-order-detail__value-text.is-mono { font-family: var(--font-mono, monospace); font-size: 12.5px; letter-spacing: .01em; }
|
||||
.aksrt-order-detail__copy { display: inline-grid; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; place-items: center; border: 1px solid var(--detail-line); border-radius: 7px; background: var(--detail-surface-3); color: var(--detail-muted); font-size: 11px; line-height: 1; cursor: pointer; transition: color .15s ease, border-color .15s ease; }
|
||||
.aksrt-order-detail__copy:hover { border-color: color-mix(in srgb, var(--detail-primary) 45%, var(--detail-line)); color: var(--detail-primary); }
|
||||
.aksrt-order-detail__copy.is-copied { border-color: color-mix(in srgb, var(--detail-success) 45%, var(--detail-line)); color: var(--detail-success); }
|
||||
|
||||
.aksrt-order-detail__refresh-state { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 10px; padding: 12px 13px; border: 1px solid color-mix(in srgb, var(--detail-warning) 26%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--detail-warning) 10%, var(--detail-surface-2)); color: var(--detail-ink); font-size: 12.5px; }
|
||||
.aksrt-order-detail__refresh-state > i { color: var(--detail-warning); font-size: 15px; }
|
||||
.aksrt-order-detail__refresh-state p { margin: 0; }
|
||||
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
|
||||
|
||||
/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts(禁封申诉弹窗同式),此处仅保留吸底定位与通栏背景。
|
||||
主题 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); }
|
||||
.aksrt-order-detail-modal .aksrt-order-detail__actions.but-average.modal-buts { margin-top: 0; }
|
||||
|
||||
/* Orders extensions */
|
||||
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||
.aksrt-orders-page__extra-link { padding: 6px 12px; border-radius: 999px; background: var(--surface-2, #f3f4f6); color: inherit; text-decoration: none; font-size: 13px; }
|
||||
.aksrt-orders-page__extra-link.is-active { background: var(--focus-color, var(--color-primary)); color: #fff; }
|
||||
.aksrt-as-list { display: grid; gap: 12px; }
|
||||
.aksrt-as-card { padding: 14px; border: 1px solid var(--main-border-color, #eee); border-radius: 10px; background: var(--main-bg-color, #fff); }
|
||||
.aksrt-as-card__head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||||
.aksrt-as-card__status { margin-left: auto; color: var(--focus-color, #2f6df6); font-weight: 600; }
|
||||
.aksrt-as-timeline { margin: 10px 0 0; padding-left: 18px; color: var(--muted-2-color, #888); font-size: 12px; }
|
||||
.aksrt-as-card__actions { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.aksrt-coupon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
|
||||
.aksrt-coupon-card { padding: 12px; border: 1px dashed var(--focus-color, #2f6df6); border-radius: 10px; background: color-mix(in srgb, var(--focus-color, #2f6df6) 6%, #fff); }
|
||||
.aksrt-coupon-card__value { color: var(--focus-color, #2f6df6); font-size: 20px; font-weight: 700; }
|
||||
.aksrt-coupon-card__code { margin: 6px 0; font-family: ui-monospace, monospace; }
|
||||
.aksrt-coupon-card__meta { margin-bottom: 4px; color: var(--muted-2-color, #888); font-size: 12px; }
|
||||
.aksrt-orders-page__field { margin-bottom: 12px; }
|
||||
.aksrt-orders-page__input, .aksrt-orders-page__select, .aksrt-orders-page__textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--main-border-color, #ddd); border-radius: 6px; }
|
||||
@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }}
|
||||
html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { background: var(--surface-1, #1a1d24); } html.dark .aksrt-orders-page__card-rail, html.dark .aksrt-orders-page__filter-panel { background: var(--surface-2, #12151b); }
|
||||
|
||||
/* ---- 售后记录卡片(订单详情弹窗) ---- */
|
||||
.aksrt-order-detail__after-sales { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
|
||||
.aksrt-order-detail__after-sale { --as-accent: var(--detail-muted); display: grid; gap: 10px; padding: 13px 14px; border: 1px solid var(--detail-line); border-left: 3px solid var(--as-accent); border-radius: 12px; background: var(--detail-surface); }
|
||||
.aksrt-order-detail__after-sale.is-pending { --as-accent: var(--detail-warning); }
|
||||
.aksrt-order-detail__after-sale.is-processing { --as-accent: var(--detail-primary); }
|
||||
.aksrt-order-detail__after-sale.is-approved, .aksrt-order-detail__after-sale.is-completed { --as-accent: var(--detail-success); }
|
||||
.aksrt-order-detail__after-sale.is-rejected, .aksrt-order-detail__after-sale.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||
.aksrt-order-detail__after-sale-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||
.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
|
||||
.aksrt-order-detail__after-sale-meta { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 10px; background: var(--detail-surface-3); }
|
||||
.aksrt-order-detail__after-sale-meta > div { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--detail-line); font-size: 12.5px; }
|
||||
.aksrt-order-detail__after-sale-meta > div:last-child { border-bottom: 0; }
|
||||
.aksrt-order-detail__after-sale-meta dt { color: var(--detail-muted); }
|
||||
.aksrt-order-detail__after-sale-meta dd { margin: 0; color: var(--detail-ink); font-size: 12.5px; text-align: right; word-break: break-word; }
|
||||
.aksrt-order-detail__after-sale-field { color: var(--detail-muted); }
|
||||
.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
/* Knowledge-payment order center */
|
||||
.aksrt-orders-page {
|
||||
--orders-ink: var(--color-ink, #1d2738);
|
||||
--orders-muted: var(--color-ink-3, #758096);
|
||||
--orders-line: color-mix(in srgb, var(--color-line, #e1e6ef) 88%, transparent);
|
||||
padding: 0;
|
||||
color: var(--orders-ink);
|
||||
}
|
||||
|
||||
.uc-orders-panel__head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(285px, 42%);
|
||||
gap: 28px;
|
||||
align-items: end;
|
||||
padding: 4px 0 22px;
|
||||
border-bottom: 1px solid var(--orders-line);
|
||||
}
|
||||
|
||||
.aksrt-orders-page__eyebrow {
|
||||
margin: 0 0 7px;
|
||||
color: var(--color-primary, #3b82f6);
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.uc-orders-panel__title { display: flex; align-items: center; gap: 9px; font-size: 21px; }
|
||||
.uc-orders-panel__title-icon { color: var(--color-primary, #3b82f6); font-size: 17px; }
|
||||
.uc-orders-panel__title-wrap > p:last-child { margin-bottom: 0; }
|
||||
|
||||
.aksrt-orders-page__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
border: 1px solid var(--orders-line);
|
||||
border-radius: 14px;
|
||||
background: var(--surface-1, #fff);
|
||||
overflow: hidden;
|
||||
}
|
||||
.aksrt-orders-page__stat { display: grid; gap: 5px; padding: 13px 15px; border-right: 1px solid var(--orders-line); }
|
||||
.aksrt-orders-page__stat:last-child { border-right: 0; }
|
||||
.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; }
|
||||
.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); }
|
||||
.aksrt-orders-page__stat strong.is-paid { color: var(--color-success, #168560); }
|
||||
|
||||
.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); }
|
||||
.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; }
|
||||
.aksrt-orders-page__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--orders-ink); font-size: 13px; }
|
||||
.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; }
|
||||
.aksrt-orders-page .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); }
|
||||
.aksrt-orders-page .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); }
|
||||
.aksrt-orders-page .btn-ghost { background: transparent; color: var(--orders-muted); }
|
||||
.aksrt-orders-page__filter-toggle { background: var(--surface-2, #f4f6f9); color: var(--orders-ink); white-space: nowrap; }
|
||||
|
||||
.aksrt-orders-page__filter-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); }
|
||||
.aksrt-orders-page__filter-panel[hidden] { display: none !important; }
|
||||
.aksrt-orders-page__filter-group { display: flex; flex: 1 1 100%; gap: 10px; align-items: center; }
|
||||
.aksrt-orders-page__filter-label { flex: 0 0 58px; color: var(--orders-muted); font-size: 12px; font-weight: 650; }
|
||||
.aksrt-orders-page__status-filters, .aksrt-orders-page__type-filters { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.aksrt-orders-page__filter-chip { display: inline-flex; gap: 5px; align-items: center; padding: 5px 9px; border: 1px solid var(--orders-line); border-radius: 999px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; }
|
||||
.aksrt-orders-page__filter-chip.is-active { border-color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-1, #fff)); color: var(--color-primary, #3b82f6); font-weight: 700; }
|
||||
.aksrt-orders-page__filter-count { display: inline-grid; min-width: 17px; height: 17px; padding: 0 4px; place-items: center; border-radius: 999px; background: var(--color-warning, #c87819); color: #fff; font-size: 10px; line-height: 1; }
|
||||
.aksrt-orders-page__clear { margin-left: auto; padding: 0; border: 0; background: transparent; color: var(--color-primary, #3b82f6); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; }
|
||||
|
||||
.aksrt-orders-page__list { display: grid; gap: 10px; }
|
||||
.aksrt-orders-page__card { overflow: hidden; border: 1px solid var(--orders-line); border-radius: 13px; background: var(--surface-1, #fff); transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease; }
|
||||
.aksrt-orders-page__card:hover { border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 36%, var(--orders-line)); box-shadow: 0 8px 22px rgba(30, 48, 79, .07); transform: translateY(-1px); }
|
||||
.aksrt-orders-page__card-rail { display: flex; gap: 10px; align-items: center; padding: 9px 14px; background: var(--surface-2, #f7f8fb); color: var(--orders-muted); font-size: 11px; }
|
||||
.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-orders-page__card-status { font-weight: 700; }
|
||||
.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); }
|
||||
.aksrt-orders-page__card-status.is-paid { color: var(--color-success, #168560); }
|
||||
.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; }
|
||||
.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; }
|
||||
.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); }
|
||||
.aksrt-orders-page__card-copy { min-width: 0; }
|
||||
.aksrt-orders-page__card-copy h3 { display: -webkit-box; margin: 0 0 5px; overflow: hidden; color: var(--orders-ink); font-size: 14px; line-height: 1.35; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.aksrt-orders-page__card-copy p { margin: 0; color: var(--orders-muted); font-family: var(--font-mono, monospace); font-size: 11px; }
|
||||
.aksrt-orders-page__card-value { display: grid; gap: 3px; text-align: right; }
|
||||
.aksrt-orders-page__card-value strong { color: var(--orders-ink); font-size: 15px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-orders-page__card-value span { color: var(--orders-muted); font-size: 11px; }
|
||||
.aksrt-orders-page__card-actions { display: flex; gap: 7px; align-items: center; justify-content: flex-end; padding: 9px 14px; border-top: 1px solid var(--orders-line); }
|
||||
.aksrt-orders-page__countdown { margin-right: auto; color: var(--color-warning, #c87819); font-size: 11px; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.aksrt-orders-page__pagination { display: flex; justify-content: center; gap: 6px; margin-top: 20px; }
|
||||
.aksrt-orders-page__page-chip { display: grid; width: 31px; height: 31px; padding: 0; place-items: center; border: 1px solid var(--orders-line); border-radius: 8px; background: var(--surface-1, #fff); color: var(--orders-muted); font: inherit; font-size: 12px; text-decoration: none; cursor: pointer; }
|
||||
.aksrt-orders-page__page-chip.is-active { border-color: var(--color-primary, #3b82f6); background: var(--color-primary, #3b82f6); color: #fff; }
|
||||
.aksrt-orders-page [data-aksrt-order-list], .aksrt-orders-page__stats, .aksrt-orders-page__pagination { transition: opacity .16s ease; }
|
||||
.aksrt-orders-page.is-loading [data-aksrt-order-list], .aksrt-orders-page.is-loading .aksrt-orders-page__stats, .aksrt-orders-page.is-loading .aksrt-orders-page__pagination { opacity: .55; pointer-events: none; }
|
||||
.aksrt-orders-page__empty { padding: 52px 20px; border: 1px dashed var(--orders-line); border-radius: 13px; background: var(--surface-2, #f7f8fb); text-align: center; }
|
||||
.aksrt-orders-page__empty i { display: block; margin-bottom: 12px; color: var(--color-primary, #3b82f6); font-size: 30px; }
|
||||
.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; }
|
||||
|
||||
/* ---- Order detail modal (front-end order center) ---- */
|
||||
/* 弹窗 portal 到 <body>,读不到页面上的 `--orders-*` 别名,因此把本模块的局部
|
||||
别名映射到 modal-runtime.css 声明的共享 portal 基座 `--modal-*`;
|
||||
路由文件不应再自行推导调色板值。 */
|
||||
.aksrt-order-detail-modal {
|
||||
--detail-ink: var(--modal-ink);
|
||||
--detail-muted: var(--modal-muted);
|
||||
--detail-line: var(--modal-line);
|
||||
--detail-surface: var(--modal-surface);
|
||||
--detail-surface-2: var(--modal-surface-2);
|
||||
--detail-surface-3: var(--modal-surface-3);
|
||||
--detail-primary: var(--modal-primary);
|
||||
--detail-warning: var(--modal-warning);
|
||||
--detail-success: var(--modal-success);
|
||||
}
|
||||
|
||||
/* 订单详情弹窗:与售后弹窗统一为 modal-mini 几何(400px);标题区改用蓝色炫彩头,运行时不再渲染默认标题栏。 */
|
||||
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||
width: min(400px, calc(100vw - 32px));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
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__skeleton { display: grid; gap: 12px; padding: 4px 0; }
|
||||
|
||||
/* Status banner — a single local variable drives the whole colour scheme. */
|
||||
.aksrt-order-detail__hero { --hero-accent: var(--detail-primary); --hero-text: color-mix(in srgb, var(--hero-accent) 72%, var(--detail-ink)); display: flex; align-items: center; gap: 13px; padding: 15px; border: 1px solid color-mix(in srgb, var(--hero-accent) 20%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--hero-accent) 12%, var(--detail-surface)); }
|
||||
.aksrt-order-detail__hero.is-pending { --hero-accent: var(--detail-warning); }
|
||||
.aksrt-order-detail__hero.is-paid { --hero-accent: var(--detail-success); }
|
||||
.aksrt-order-detail__hero.is-refunded, .aksrt-order-detail__hero.is-closed { --hero-accent: var(--detail-muted); }
|
||||
.aksrt-order-detail__hero-icon { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; border-radius: 12px; background: color-mix(in srgb, var(--hero-accent) 16%, transparent); color: var(--hero-text); font-size: 19px; }
|
||||
.aksrt-order-detail__hero-copy { display: grid; flex: 1 1 auto; gap: 4px; min-width: 0; }
|
||||
.aksrt-order-detail__status { color: var(--hero-text); font-size: 15px; font-weight: 750; line-height: 1.2; }
|
||||
.aksrt-order-detail__desc { margin: 0; color: var(--detail-muted); font-size: 12px; line-height: 1.45; }
|
||||
.aksrt-order-detail__countdown { color: var(--hero-text); font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__hero-amount { display: grid; flex: 0 0 auto; gap: 3px; justify-items: end; text-align: right; }
|
||||
.aksrt-order-detail__hero-amount strong { color: var(--detail-ink); font-size: 21px; font-weight: 750; line-height: 1.1; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__hero-amount span { color: var(--detail-muted); font-size: 11px; }
|
||||
|
||||
.aksrt-order-detail__group { display: grid; gap: 10px; }
|
||||
.aksrt-order-detail__group-title { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--detail-ink); font-size: 13px; font-weight: 700; line-height: 1.3; }
|
||||
.aksrt-order-detail__group-title::before { content: ""; width: 3px; height: 13px; border-radius: 2px; background: var(--detail-primary); }
|
||||
|
||||
.aksrt-order-detail__product { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 12px; align-items: center; padding: 12px; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); }
|
||||
.aksrt-order-detail__thumb { width: 54px; height: 54px; border-radius: 10px; object-fit: cover; }
|
||||
.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--detail-surface-2); color: var(--detail-primary); font-size: 19px; }
|
||||
.aksrt-order-detail__product-copy { display: grid; gap: 6px; min-width: 0; }
|
||||
.aksrt-order-detail__product-title { display: -webkit-box; overflow: hidden; color: var(--detail-ink); font-size: 14px; line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.aksrt-order-detail__product-meta { display: inline-flex; justify-self: start; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--detail-primary) 9%, var(--detail-surface-2)); color: var(--detail-primary); font-size: 11px; font-weight: 650; line-height: 1.5; }
|
||||
|
||||
.aksrt-order-detail__list { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 12px; background: var(--detail-surface); }
|
||||
.aksrt-order-detail__list > div { display: grid; grid-template-columns: 84px minmax(0, 1fr); align-items: center; gap: 12px; padding: 11px 13px; border-bottom: 1px solid var(--detail-line); }
|
||||
.aksrt-order-detail__list > div:last-child { border-bottom: 0; }
|
||||
.aksrt-order-detail__list dt { color: var(--detail-muted); font-size: 12.5px; }
|
||||
.aksrt-order-detail__list dd { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; margin: 0; color: var(--detail-ink); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; }
|
||||
.aksrt-order-detail__value-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.aksrt-order-detail__value-text.is-mono { font-family: var(--font-mono, monospace); font-size: 12.5px; letter-spacing: .01em; }
|
||||
.aksrt-order-detail__copy { display: inline-grid; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; place-items: center; border: 1px solid var(--detail-line); border-radius: 7px; background: var(--detail-surface-3); color: var(--detail-muted); font-size: 11px; line-height: 1; cursor: pointer; transition: color .15s ease, border-color .15s ease; }
|
||||
.aksrt-order-detail__copy:hover { border-color: color-mix(in srgb, var(--detail-primary) 45%, var(--detail-line)); color: var(--detail-primary); }
|
||||
.aksrt-order-detail__copy.is-copied { border-color: color-mix(in srgb, var(--detail-success) 45%, var(--detail-line)); color: var(--detail-success); }
|
||||
|
||||
.aksrt-order-detail__refresh-state { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 10px; padding: 12px 13px; border: 1px solid color-mix(in srgb, var(--detail-warning) 26%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--detail-warning) 10%, var(--detail-surface-2)); color: var(--detail-ink); font-size: 12.5px; }
|
||||
.aksrt-order-detail__refresh-state > i { color: var(--detail-warning); font-size: 15px; }
|
||||
.aksrt-order-detail__refresh-state p { margin: 0; }
|
||||
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
|
||||
|
||||
/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts,此处仅保留吸底定位与通栏背景。
|
||||
主题 modal-buts 自带 margin-top:16px,吸底场景下用更高优先级清掉,避免内容与吸底条之间出现空隙。
|
||||
footer 不留垂直内边距(水平同理归零)——上下 13px 并入按钮(min-height 64 = 13+38+13),
|
||||
按钮盒即整个底部区域;安全区留在 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 */
|
||||
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||
.aksrt-orders-page__extra-link { padding: 6px 12px; border-radius: 999px; background: var(--surface-2, #f3f4f6); color: inherit; text-decoration: none; font-size: 13px; }
|
||||
.aksrt-orders-page__extra-link.is-active { background: var(--focus-color, var(--color-primary)); color: #fff; }
|
||||
.aksrt-as-list { display: grid; gap: 12px; }
|
||||
.aksrt-as-card { padding: 14px; border: 1px solid var(--main-border-color, #eee); border-radius: 10px; background: var(--main-bg-color, #fff); }
|
||||
.aksrt-as-card__head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||||
.aksrt-as-card__status { margin-left: auto; color: var(--focus-color, #2f6df6); font-weight: 600; }
|
||||
.aksrt-as-timeline { margin: 10px 0 0; padding-left: 18px; color: var(--muted-2-color, #888); font-size: 12px; }
|
||||
.aksrt-as-card__actions { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.aksrt-coupon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
|
||||
.aksrt-coupon-card { padding: 12px; border: 1px dashed var(--focus-color, #2f6df6); border-radius: 10px; background: color-mix(in srgb, var(--focus-color, #2f6df6) 6%, #fff); }
|
||||
.aksrt-coupon-card__value { color: var(--focus-color, #2f6df6); font-size: 20px; font-weight: 700; }
|
||||
.aksrt-coupon-card__code { margin: 6px 0; font-family: ui-monospace, monospace; }
|
||||
.aksrt-coupon-card__meta { margin-bottom: 4px; color: var(--muted-2-color, #888); font-size: 12px; }
|
||||
.aksrt-orders-page__field { margin-bottom: 12px; }
|
||||
.aksrt-orders-page__input, .aksrt-orders-page__select, .aksrt-orders-page__textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--main-border-color, #ddd); border-radius: 6px; }
|
||||
@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }}
|
||||
html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { background: var(--surface-1, #1a1d24); } html.dark .aksrt-orders-page__card-rail, html.dark .aksrt-orders-page__filter-panel { background: var(--surface-2, #12151b); }
|
||||
|
||||
/* ---- 售后记录卡片(订单详情弹窗) ---- */
|
||||
.aksrt-order-detail__after-sales { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
|
||||
.aksrt-order-detail__after-sale { --as-accent: var(--detail-muted); display: grid; gap: 10px; padding: 13px 14px; border: 1px solid var(--detail-line); border-left: 3px solid var(--as-accent); border-radius: 12px; background: var(--detail-surface); }
|
||||
.aksrt-order-detail__after-sale.is-pending { --as-accent: var(--detail-warning); }
|
||||
.aksrt-order-detail__after-sale.is-processing { --as-accent: var(--detail-primary); }
|
||||
.aksrt-order-detail__after-sale.is-approved, .aksrt-order-detail__after-sale.is-completed { --as-accent: var(--detail-success); }
|
||||
.aksrt-order-detail__after-sale.is-rejected, .aksrt-order-detail__after-sale.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||
.aksrt-order-detail__after-sale-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||
.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
|
||||
.aksrt-order-detail__after-sale-field { color: var(--detail-muted); }
|
||||
.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
@@ -125,6 +125,8 @@
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
/* 用户中心头图卡保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
|
||||
@@ -478,18 +478,6 @@
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-softer);
|
||||
}
|
||||
.binding-item__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: var(--color-ink);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.binding-item__icon i {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
.binding-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -190,6 +190,25 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 推广海报弹窗壳与上面两个同构(服务端静态输出 .modal.flex.jc.fade),
|
||||
静止态同样必须显式隐藏,否则 .flex 工具类会使其铺满全屏形成隐形遮罩,
|
||||
拦截整页点击。打开态由 initAffiliatePoster 的内联 display:flex 驱动,
|
||||
关闭撤除内联样式后即回到隐藏规则。 */
|
||||
.modal[data-aksrt-affiliate-poster-modal] {
|
||||
display: none;
|
||||
}
|
||||
.modal[data-aksrt-affiliate-poster-modal].in {
|
||||
display: flex;
|
||||
}
|
||||
/* 海报弹窗的圆形按钮必须保持圆形。上面的 .uc-content .but.jb-blue 是
|
||||
(0,3,0) 且后加载,会盖掉 main.css 的 .poster-share .but.cir(0,2,0),
|
||||
把蓝色下载钮从圆角 5em 压成 4px 方角;黄色关闭钮因为不在该规则内
|
||||
才幸免。这里按 (0,3,0) 同权重回位参考主题的 pill 形态。 */
|
||||
.modal[data-aksrt-affiliate-poster-modal] .but.cir {
|
||||
padding: 0;
|
||||
border-radius: 5em;
|
||||
}
|
||||
|
||||
.balance-charge-form .zib-widget {
|
||||
clear: both;
|
||||
background-color: var(--main-bg-color);
|
||||
@@ -304,12 +323,24 @@
|
||||
body.uc-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.but.jb-blue,
|
||||
.but.b-blue-2 {
|
||||
/* Scoped to the user-center content shell. These are the credits panel's own
|
||||
* button skins, and every element they target renders inside .uc-content (the
|
||||
* panels and their modals are all emitted through templates/parts/user-center/
|
||||
* panel.php, which templates/pages/page-profile.php wraps in .uc-content).
|
||||
*
|
||||
* Left unscoped they are (0,2,0) rules that load after public.css, so they
|
||||
* out-rank the legacy .but.radius pill and flatten every .but.jb-blue on the
|
||||
* page down to 4px. The top-nav publish chip is .but.jb-blue.radius, so on the
|
||||
* user-center route it lost its 50px pill and no longer matched the homepage.
|
||||
* Site-wide .but.jb-blue callers outside the panel (login, submit, badge and
|
||||
* cashier buttons) also picked up the flat #3b82f6 skin instead of the legacy
|
||||
* gradient.
|
||||
*
|
||||
* The .btn-block restatement that used to sit here was byte-identical to the
|
||||
* one in core/buttons.css, which loads earlier on every page, so it could never
|
||||
* win and is dropped rather than scoped. */
|
||||
.uc-content .but.jb-blue,
|
||||
.uc-content .but.b-blue-2 {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
@@ -317,18 +348,18 @@ body.uc-modal-open {
|
||||
padding: 0.45em 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.but.c-blue-2.hollow,
|
||||
.but.c-green.hollow {
|
||||
.uc-content .but.c-blue-2.hollow,
|
||||
.uc-content .but.c-green.hollow {
|
||||
background: transparent;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 4px;
|
||||
padding: 0.4em 0.9em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.but.c-blue-2 {
|
||||
.uc-content .but.c-blue-2 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.but.c-green {
|
||||
.uc-content .but.c-green {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,13 +110,6 @@
|
||||
gap: 4px 16px;
|
||||
}
|
||||
|
||||
/* 待偿欠款:退款收回不足挂账,副信息里以描边标签弱提示。 */
|
||||
.uc-content .uc-commission-card__debt {
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .22);
|
||||
}
|
||||
|
||||
/* 立即提现按钮:贴右居中,对齐参考主题 .abs-right。 */
|
||||
.uc-content .uc-commission-card__action {
|
||||
position: absolute;
|
||||
@@ -214,9 +207,3 @@
|
||||
.uc-content .uc-revenue-summary-row--explain { grid-template-columns: 1fr; }
|
||||
.uc-content .uc-revenue-summary-row--explain .uc-reward-richtext { grid-column: 1; }
|
||||
}
|
||||
|
||||
/* 推广海报预览弹窗:舞台居中,canvas 自适应弹窗宽度。 */
|
||||
.uc-content .uc-affiliate-poster { display: grid; place-items: center; min-height: 120px; }
|
||||
.uc-content .uc-affiliate-poster img,
|
||||
.uc-content .uc-affiliate-poster canvas { display: block; max-width: 100%; height: auto; margin: 0 auto; border-radius: 12px; background: var(--surface-2, #f7f8fa); }
|
||||
.uc-content .uc-affiliate-poster p { margin: 0; text-align: center; }
|
||||
@@ -264,8 +264,6 @@
|
||||
.aksrt-account-settings__section--social .binding-captcha { margin-bottom: 12px; }
|
||||
.aksrt-account-list--social .binding-item { border: 0; }
|
||||
.aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; }
|
||||
.aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); }
|
||||
.aksrt-account-list--social .binding-item__icon i { font-size: 22px; line-height: 1; }
|
||||
.aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; }
|
||||
.aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
|
||||
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
|
||||
|
||||
@@ -622,25 +622,6 @@ body.aksrt-user-center .uc-content,
|
||||
}
|
||||
}
|
||||
|
||||
.uc-service-grid__icon small {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -7px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border: 2px solid var(--surface-1);
|
||||
border-radius: 99px;
|
||||
background: var(--color-error, #ef4444);
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.uc-sidebar__pair {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -684,7 +665,8 @@ body.aksrt-user-center .uc-content,
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.uc-summary-card__body > i {
|
||||
.uc-summary-card__body > i,
|
||||
.uc-summary-card__body > svg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -716,14 +698,26 @@ body.aksrt-user-center .uc-content,
|
||||
|
||||
.uc-center-card strong { font-size: 13px; }
|
||||
.uc-center-card small { margin-top: 3px; color: var(--color-ink-4); font-size: 10px; }
|
||||
.uc-center-card > i { color: var(--color-ink-4); font-size: 11px; }
|
||||
.uc-center-card > i,
|
||||
.uc-center-card > svg { color: var(--color-ink-4); font-size: 11px; }
|
||||
.uc-center-card--creation .uc-center-card__icon { color: #6650c7; background: rgba(124, 92, 220, 0.12); }
|
||||
.uc-center-card--revenue .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); }
|
||||
|
||||
.uc-service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
/* Services / settings grids, ported from the reference theme sidebar:
|
||||
four per row, no tinted tile behind the icon. */
|
||||
.user-icon-but-box > a {
|
||||
width: calc(25% - 10px);
|
||||
margin: 5px;
|
||||
min-width: 50px;
|
||||
max-width: 100px;
|
||||
color: var(--color-ink-3);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-icon-but-box > a:hover,
|
||||
.user-icon-but-box > a.is-active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.uc-follow-management__list { display: grid; gap: 10px; margin-top: 16px; }
|
||||
@@ -735,44 +729,3 @@ body.aksrt-user-center .uc-content,
|
||||
.uc-follow-management__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
|
||||
@media (max-width: 560px) { .uc-follow-management__item { align-items: flex-start; flex-wrap: wrap; } .uc-follow-management__actions { width: 100%; margin-left: 60px; } }
|
||||
|
||||
.uc-service-grid__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
padding: 4px 2px;
|
||||
color: var(--color-ink-3);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.uc-service-grid__icon {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 14px;
|
||||
background: var(--surface-2);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uc-service-grid__item--cart .uc-service-grid__icon { color: #dd6941; background: rgba(238, 109, 61, 0.12); }
|
||||
.uc-service-grid__item--level .uc-service-grid__icon { color: #9b6b0a; background: rgba(234, 179, 8, 0.13); }
|
||||
.uc-service-grid__item--revenue .uc-service-grid__icon { color: #b8377d; background: rgba(219, 39, 119, 0.12); }
|
||||
.uc-service-grid__item--auth .uc-service-grid__icon { color: #287bc4; background: rgba(14, 165, 233, 0.12); }
|
||||
.uc-service-grid__item--orders .uc-service-grid__icon { color: #c05621; background: rgba(234, 88, 12, 0.12); }
|
||||
.uc-service-grid__item--notice .uc-service-grid__icon { color: #d04b65; background: rgba(244, 63, 94, 0.11); }
|
||||
.uc-service-grid__item--profile .uc-service-grid__icon { color: #6550c7; background: rgba(124, 92, 220, 0.12); }
|
||||
.uc-service-grid__item--security .uc-service-grid__icon { color: #16816c; background: rgba(16, 185, 129, 0.12); }
|
||||
|
||||
.uc-service-grid__item:hover,
|
||||
.uc-service-grid__item.is-active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.uc-service-grid__item.is-active .uc-service-grid__icon {
|
||||
box-shadow: 0 0 0 2px var(--color-primary-soft), 0 5px 14px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
@@ -88,6 +88,8 @@
|
||||
.aksrt-author-hero__avatar img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
/* 公开主页 hero 头像保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||
border-radius: var(--radius);
|
||||
border: 4px solid var(--surface-1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
|
||||
+96
-16
@@ -38,6 +38,14 @@
|
||||
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
||||
}
|
||||
|
||||
function formatCutCount(value) {
|
||||
var number = Number(value);
|
||||
if (!Number.isFinite(number)) return String(value);
|
||||
if (number > 999999) return String(Math.round(number / 10000)) + 'W+';
|
||||
if (number > 9999) return String(Math.round(number / 1000) / 10) + 'W+';
|
||||
return String(number);
|
||||
}
|
||||
|
||||
function api(path, options) {
|
||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||
return {
|
||||
@@ -64,16 +72,18 @@
|
||||
var walletNumber = Number(wallet);
|
||||
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
||||
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
||||
element.textContent = walletText;
|
||||
element.title = walletText;
|
||||
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText;
|
||||
element.textContent = text;
|
||||
element.title = text;
|
||||
});
|
||||
}
|
||||
if (points !== undefined && points !== null && points !== '') {
|
||||
var pointsNumber = Number(points);
|
||||
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
||||
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
||||
element.textContent = pointsText;
|
||||
element.title = pointsText;
|
||||
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(points) : pointsText;
|
||||
element.textContent = text;
|
||||
element.title = text;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -140,19 +150,62 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Referral poster (canvas preview modal: hero image + copy + QR + link) =====
|
||||
// ===== Referral poster =====
|
||||
// 弹窗形态 1:1 复刻参考主题 poster-share.js:Bootstrap 语义(.in 显隐 +
|
||||
// .modal-backdrop + body.modal-open + ESC),与下方充值弹窗同构,不复依赖
|
||||
// jQuery/Bootstrap 运行时。壳上带 .flex.jc,故打开态用 flex 而非 block。
|
||||
function initAffiliatePoster(root, url, poster, siteName) {
|
||||
var button = root.querySelector('[data-affiliate-poster]');
|
||||
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
|
||||
var stage = document.querySelector('[data-affiliate-poster-stage]');
|
||||
var download = document.querySelector('[data-affiliate-poster-download]');
|
||||
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
|
||||
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
|
||||
var content = modal ? modal.querySelector('.modal-content') : null;
|
||||
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
|
||||
if (!button || !modal) return;
|
||||
if (!url || !poster || poster.enabled !== true) return;
|
||||
button.hidden = false;
|
||||
[].forEach.call(modal.querySelectorAll('[data-affiliate-poster-close]'), function (closer) {
|
||||
closer.addEventListener('click', function () { closeModal(modal); });
|
||||
});
|
||||
|
||||
var backdrop = null;
|
||||
var canvas = null;
|
||||
|
||||
function openPosterModal() {
|
||||
if (modal.classList.contains('in')) return;
|
||||
modal.style.display = 'flex';
|
||||
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
|
||||
modal.classList.add('in');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
if (!backdrop) {
|
||||
backdrop = document.createElement('div');
|
||||
backdrop.className = 'modal-backdrop fade in';
|
||||
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
|
||||
document.body.appendChild(backdrop);
|
||||
}
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
function closePosterModal() {
|
||||
if (!modal.classList.contains('in')) return;
|
||||
modal.classList.remove('in');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
document.body.classList.remove('modal-open');
|
||||
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
|
||||
backdrop = null;
|
||||
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
|
||||
window.setTimeout(function () {
|
||||
if (!modal.classList.contains('in')) modal.style.display = '';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
modal.addEventListener('click', function (event) {
|
||||
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
|
||||
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
|
||||
closePosterModal();
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
|
||||
});
|
||||
|
||||
if (download) {
|
||||
download.addEventListener('click', function () {
|
||||
if (!canvas) return;
|
||||
@@ -168,20 +221,47 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
button.addEventListener('click', function () {
|
||||
if (typeof window.qrcode !== 'function') {
|
||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
|
||||
openModal(modal);
|
||||
openPosterModal();
|
||||
return;
|
||||
}
|
||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '正在生成海报…' + '</p>'; }
|
||||
if (download) download.hidden = true;
|
||||
openModal(modal);
|
||||
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
|
||||
if (loading) loading.style.display = '';
|
||||
if (download) download.classList.add('hide');
|
||||
openPosterModal();
|
||||
button.disabled = true;
|
||||
buildPosterCanvas(url, poster, siteName).then(function (built) {
|
||||
canvas = built;
|
||||
if (stage) { stage.innerHTML = ''; stage.appendChild(built); }
|
||||
if (download) download.hidden = false;
|
||||
if (!stage) return;
|
||||
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
|
||||
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
|
||||
var img = document.createElement('img');
|
||||
img.src = canvas.toDataURL('image/png');
|
||||
img.alt = '推广海报';
|
||||
img.className = 'loaded-img';
|
||||
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
|
||||
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
|
||||
img.width = 768 * 0.7;
|
||||
img.height = 1168 * 0.7;
|
||||
stage.appendChild(img);
|
||||
stage.classList.add('loaded');
|
||||
if (download) {
|
||||
download.href = img.src;
|
||||
download.classList.remove('hide');
|
||||
}
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (content) {
|
||||
content.style.height = stage.offsetHeight + 'px';
|
||||
content.style.transition = '.2s';
|
||||
window.setTimeout(function () {
|
||||
content.style.height = '';
|
||||
content.style.overflow = '';
|
||||
content.style.transition = '';
|
||||
}, 200);
|
||||
}
|
||||
}).catch(function () {
|
||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
|
||||
}).finally(function () {
|
||||
|
||||
@@ -103,7 +103,6 @@
|
||||
this.bindFeatured();
|
||||
this.bindPay();
|
||||
this.bindLeaveWarning();
|
||||
this.bindAi();
|
||||
this.bindTagActive();
|
||||
this.loadFeaturedFromDom();
|
||||
},
|
||||
@@ -635,67 +634,6 @@
|
||||
});
|
||||
},
|
||||
|
||||
bindAi: function () {
|
||||
var ai = cfg.ai || {};
|
||||
if (!ai.available) {
|
||||
return;
|
||||
}
|
||||
qsa('.aksrt-ai-assist-btn').forEach(function (btn) {
|
||||
btn.hidden = false;
|
||||
btn.addEventListener('click', function () {
|
||||
var action = btn.getAttribute('data-ai-action');
|
||||
var content = getEditorContent();
|
||||
if (!content || content.replace(/<[^>]+>/g, '').trim().length < 10) {
|
||||
notyf(i18n.needContent || '请先填写正文内容', 'warning');
|
||||
return;
|
||||
}
|
||||
var toastId = action === 'tags'
|
||||
? 'aksrt-front-submit-ai-tags-toast'
|
||||
: 'aksrt-front-submit-ai-summary-toast';
|
||||
var old = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = i18n.aiGenerating || '...';
|
||||
notyf(i18n.aiGenerating || '正在生成…', 'load', '', toastId);
|
||||
fetch((cfg.restUrl || '').replace(/\/$/, '') + '/ai/' + action, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': cfg.restNonce || '',
|
||||
},
|
||||
body: JSON.stringify({ content: content }),
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (res) {
|
||||
var text = (res && (res.data || res).text) || (res && res.result) || '';
|
||||
if (!text) {
|
||||
throw new Error(i18n.aiFailed || 'AI failed');
|
||||
}
|
||||
if (action === 'summary') {
|
||||
var ex = qs('[name="excerpt"]');
|
||||
if (ex) {
|
||||
ex.value = text;
|
||||
}
|
||||
} else if (action === 'tags') {
|
||||
var tags = qs('[name="tags"]');
|
||||
if (tags) {
|
||||
tags.value = text;
|
||||
}
|
||||
}
|
||||
notyf(i18n.aiFilled || '已填入,可继续编辑', 'success', 5, toastId);
|
||||
})
|
||||
.catch(function (err) {
|
||||
notyf(err.message || i18n.aiFailed || 'AI 生成失败', 'danger', 5, toastId);
|
||||
})
|
||||
.finally(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = old;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
||||
|
||||
+700
-546
File diff suppressed because it is too large.
Load diff
+712
-756
File diff suppressed because it is too large.
Load diff
+110
-23
@@ -17,6 +17,14 @@
|
||||
return el;
|
||||
}
|
||||
|
||||
// Server-side schema sanitization limits these configured rich-text fields to post-safe HTML.
|
||||
function htmlNode(tag, className, value) {
|
||||
var el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (value !== undefined && value !== null) el.innerHTML = text(value);
|
||||
return el;
|
||||
}
|
||||
|
||||
function imageNode(src, className) {
|
||||
if (!src) return null;
|
||||
var image = document.createElement("img");
|
||||
@@ -61,7 +69,7 @@
|
||||
if (levelInfo && levelInfo.img_icon) {
|
||||
var img = node("img", "img-icon");
|
||||
img.src = text(levelInfo.img_icon);
|
||||
img.alt = text(levelInfo.name || "");
|
||||
img.alt = text(levelInfo.name_text || levelInfo.name || "");
|
||||
img.decoding = "async";
|
||||
return img;
|
||||
}
|
||||
@@ -115,7 +123,8 @@
|
||||
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
||||
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
||||
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
||||
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg jb-yellow", item.discount_tag));
|
||||
// 标签颜色取后台「标签颜色」配置,未配置时回退参考主题默认的 jb-yellow。
|
||||
if (item.discount_tag) append(product, htmlNode("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag));
|
||||
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
|
||||
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
||||
append(label, product);
|
||||
@@ -179,7 +188,7 @@
|
||||
box.setAttribute("data-condition", "==");
|
||||
box.setAttribute("data-value", "card_pass");
|
||||
if (defaultMethod !== "card_pass") box.style.display = "none";
|
||||
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
||||
if (cardPass.desc) append(box, htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
||||
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
||||
if (!cardPass.only_password) {
|
||||
var cardInput = node("input", "form-control mb6");
|
||||
@@ -384,12 +393,7 @@
|
||||
var avatarItem = node("li");
|
||||
var avatarWrap = node("div", "avatar-img");
|
||||
if (member.avatar_url) {
|
||||
var avatar = node("img");
|
||||
avatar.src = text(member.avatar_url);
|
||||
avatar.alt = "";
|
||||
avatar.width = 42;
|
||||
avatar.height = 42;
|
||||
append(avatarWrap, avatar);
|
||||
append(avatarWrap, imageNode(text(member.avatar_url), "avatar"));
|
||||
}
|
||||
append(avatarItem, avatarWrap);
|
||||
append(userBox, avatarItem);
|
||||
@@ -403,7 +407,7 @@
|
||||
} else if (member.state === "expired") {
|
||||
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
||||
} else if (catalog.desc) {
|
||||
append(userInfo, node("div", "muted-2-color em09", catalog.desc));
|
||||
append(userInfo, htmlNode("div", "muted-2-color em09", catalog.desc));
|
||||
}
|
||||
append(userBox, userInfo);
|
||||
append(modal, userBox);
|
||||
@@ -439,8 +443,8 @@
|
||||
append(miniIcon, vipIcon(levelInfo));
|
||||
append(miniIconbox, miniIcon);
|
||||
var miniInfo = node("div", "v-info");
|
||||
append(miniInfo, node("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
||||
append(miniInfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(miniInfo, htmlNode("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
||||
append(miniInfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(miniIconbox, miniInfo);
|
||||
append(miniCard, miniIconbox);
|
||||
append(tab, miniCard);
|
||||
@@ -466,15 +470,12 @@
|
||||
append(vicon, vipIcon(levelInfo));
|
||||
append(iconbox, vicon);
|
||||
var vinfo = node("div", "v-info");
|
||||
append(vinfo, node("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
||||
append(vinfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(vinfo, htmlNode("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
||||
append(vinfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(iconbox, vinfo);
|
||||
append(info, iconbox);
|
||||
if (levelInfo.equity) {
|
||||
var equity = document.createElement("ul");
|
||||
equity.className = "v-equity";
|
||||
equity.innerHTML = text(levelInfo.equity);
|
||||
append(info, equity);
|
||||
append(info, htmlNode("div", "", levelInfo.equity));
|
||||
}
|
||||
var infoBa = node("div", "abs-center vip-baicon");
|
||||
append(infoBa, vipIcon(levelInfo));
|
||||
@@ -497,7 +498,7 @@
|
||||
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
||||
append(products, pointsLink);
|
||||
}
|
||||
if (catalog.more) append(products, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||
if (catalog.more) append(products, htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||
renderMethods(products, methods, cardPass, wallet);
|
||||
var actionName = actionKey(action);
|
||||
var actionAllowed = allowed[actionName] === true;
|
||||
@@ -524,8 +525,6 @@
|
||||
append(modal, tabs);
|
||||
append(modal, content);
|
||||
|
||||
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
|
||||
|
||||
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
|
||||
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
||||
applyVipMethodVisibility(pane);
|
||||
@@ -703,6 +702,46 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 骨架屏:仿弹窗真实布局(用户框 + 等级卡 + 双列套餐面板)。
|
||||
// 线条/色块复用全局骨架类(build/public.min.css),VIP 专属形状见 vip-modal.css。
|
||||
function vipSkeleton() {
|
||||
var skeleton = node("div", "aksrt-vip-modal-skeleton");
|
||||
skeleton.setAttribute("aria-live", "polite");
|
||||
skeleton.setAttribute("aria-busy", "true");
|
||||
|
||||
// 用户框:头像圆 + 两行文字。
|
||||
var user = node("div", "aksrt-vip-modal-skeleton__user");
|
||||
append(user, node("span", "aksrt-vip-modal-skeleton__avatar"));
|
||||
var userLines = node("span", "aksrt-vip-modal-skeleton__lines");
|
||||
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||
append(user, userLines);
|
||||
append(skeleton, user);
|
||||
|
||||
// 等级卡行(vip-cardminis):两张 tab 占位。
|
||||
var tabs = node("div", "aksrt-vip-modal-skeleton__tabs");
|
||||
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||
append(skeleton, tabs);
|
||||
|
||||
// 双列面板:左权益信息 + 右套餐区(套餐卡、支付方式行、提交按钮条)。
|
||||
var pane = node("div", "aksrt-vip-modal-skeleton__pane");
|
||||
var info = node("div", "aksrt-vip-modal-skeleton__info");
|
||||
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||
append(info, node("span", "aksrt-modal-runtime__skeleton-line"));
|
||||
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||
append(pane, info);
|
||||
var products = node("div", "aksrt-vip-modal-skeleton__products");
|
||||
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||
append(products, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||
append(products, node("span", "aksrt-vip-modal-skeleton__button"));
|
||||
append(pane, products);
|
||||
append(skeleton, pane);
|
||||
|
||||
return skeleton.outerHTML;
|
||||
}
|
||||
|
||||
function vipPay(trigger) {
|
||||
var level = trigger
|
||||
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
||||
@@ -711,11 +750,11 @@
|
||||
url: "/pay/vip/modal",
|
||||
query: { vip_level: level },
|
||||
className: "aksrt-vip-modal-shell",
|
||||
skeleton: '<div class="aksrt-vip-modal-panel"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>',
|
||||
skeleton: vipSkeleton(),
|
||||
telemetry: "vip",
|
||||
kind: "vip",
|
||||
responseType: "json",
|
||||
cacheTtl: 15000,
|
||||
cacheTtl: 60000,
|
||||
cacheTags: ["vip"],
|
||||
renderData: function (data, session) {
|
||||
renderModalView(session.content, data);
|
||||
@@ -733,6 +772,53 @@
|
||||
});
|
||||
}
|
||||
|
||||
var prefetchedLevels = Object.create(null);
|
||||
|
||||
// 与 openRemote 同一 cacheKey(method + AksrtRequest.url),命中同一份缓存/flight。
|
||||
function vipModalCacheKey(level) {
|
||||
return "GET:" + window.AksrtRequest.url("/pay/vip/modal", { vip_level: level });
|
||||
}
|
||||
|
||||
// 意图预热:悬停/聚焦开通入口时按等级预热弹窗数据(不打开弹窗,失败静默)。
|
||||
// 点击打开时若预热请求在途,cache.acquire 的 flight 去重会自动合并。
|
||||
function prefetchVipModal(trigger) {
|
||||
if (!trigger || !window.AksrtModalCache || !window.AksrtRequest) return;
|
||||
var level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
|
||||
if (level < 1 || level > 2) level = 1;
|
||||
if (prefetchedLevels[level]) return;
|
||||
prefetchedLevels[level] = true;
|
||||
var cache = window.AksrtModalCache;
|
||||
if (cache.get && cache.get(vipModalCacheKey(level)).hit) return;
|
||||
try {
|
||||
cache.acquire(vipModalCacheKey(level), function () {
|
||||
return window.AksrtRequest.request("/pay/vip/modal", {
|
||||
method: "GET",
|
||||
query: { vip_level: level },
|
||||
});
|
||||
}, {
|
||||
ttl: 60000,
|
||||
tags: ["vip"],
|
||||
});
|
||||
} catch (error) {
|
||||
// 预热失败不影响点击后的正常打开路径。
|
||||
}
|
||||
}
|
||||
|
||||
function vipTriggerFrom(event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.closest) return null;
|
||||
return target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||
}
|
||||
|
||||
function bindPrefetch() {
|
||||
// pointerenter 不冒泡但走捕获阶段;focusin 冒泡,两者互补覆盖鼠标与键盘。
|
||||
["pointerenter", "focusin"].forEach(function (type) {
|
||||
document.addEventListener(type, function (event) {
|
||||
prefetchVipModal(vipTriggerFrom(event));
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
function onDocClick(event) {
|
||||
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||
if (!trigger) return;
|
||||
@@ -773,6 +859,7 @@
|
||||
if (window.__aksrtPayVipBound) return;
|
||||
window.__aksrtPayVipBound = true;
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
bindPrefetch();
|
||||
bindPointsModal();
|
||||
bindPointsEntry();
|
||||
}
|
||||
|
||||
@@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
|
||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
|
||||
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Core\SEOHead')) {
|
||||
\AKSRT\Core\SEOHead::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_normalize_header_cta_label')) :
|
||||
/**
|
||||
* Normalize CTA label HTML: map FA5 classes and keep a single leading icon.
|
||||
*/
|
||||
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fa fas fa-pen'): string {
|
||||
$label = trim($label);
|
||||
if ($label === '') {
|
||||
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
|
||||
}
|
||||
|
||||
if (stripos($label, '<i') === false) {
|
||||
// No space between icon and label — spacing via CSS margin (legacy .but > .fa).
|
||||
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i><span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
|
||||
} else {
|
||||
$label = preg_replace(
|
||||
'/(<\/i>)\s*([^<].*)$/u',
|
||||
'$1<span class="navbar-cta-text">$2</span>',
|
||||
$label
|
||||
) ?? $label;
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_new_add_btns')) :
|
||||
/**
|
||||
* Multi-type publish button (legacy newadd-btns).
|
||||
* Publish chip (legacy newadd-btns, single action).
|
||||
*
|
||||
* Mirrors the single-button branch of the reference zib_get_new_add_btns():
|
||||
* the paint classes ride on the anchor itself and the label is emitted
|
||||
* verbatim. Only the post action exists, so there is no dropdown branch.
|
||||
*
|
||||
* @param string[] $types
|
||||
*/
|
||||
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
||||
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$actions = aksrt_get_new_add_actions($types);
|
||||
if (!$actions) {
|
||||
// Last resort: keep a write-post entry so the top-nav chip is never blank.
|
||||
$fallback = function_exists('aksrt_get_new_post_url') ? (string) aksrt_get_new_post_url() : '';
|
||||
if ($fallback === '') {
|
||||
$fallback = admin_url('post-new.php');
|
||||
}
|
||||
$actions = [[
|
||||
'type' => 'post',
|
||||
'url' => $fallback,
|
||||
'label' => __('写文章', 'aksrt'),
|
||||
'icon' => 'fa fas fa-file-alt',
|
||||
]];
|
||||
return '';
|
||||
}
|
||||
|
||||
$a = $actions[0];
|
||||
if ($label === '') {
|
||||
$label = __('发布', 'aksrt');
|
||||
}
|
||||
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
|
||||
|
||||
// Split paint classes vs wrapper tokens.
|
||||
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$cta_tokens = [];
|
||||
$wrap_tokens = ['newadd-btns'];
|
||||
foreach ($class_tokens as $token) {
|
||||
if ($token === 'newadd-btns') {
|
||||
continue;
|
||||
}
|
||||
if ($token === 'btn-primary' || $token === 'but' || $token === 'navbar-cta' || $token === 'radius' || $token === 'main-radius' || $token === 'mini-radius' || str_starts_with($token, 'jb-') || str_starts_with($token, 'b-')) {
|
||||
$cta_tokens[] = $token;
|
||||
continue;
|
||||
}
|
||||
$wrap_tokens[] = $token;
|
||||
}
|
||||
if (!$cta_tokens) {
|
||||
$cta_tokens[] = 'jb-blue';
|
||||
}
|
||||
if (!in_array('but', $cta_tokens, true)) {
|
||||
$cta_tokens[] = 'but';
|
||||
}
|
||||
$cta_class = trim(implode(' ', array_unique($cta_tokens)));
|
||||
|
||||
if (count($actions) === 1) {
|
||||
$a = $actions[0];
|
||||
return '<a rel="nofollow" class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' ' . $cta_class . ' navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
|
||||
. $label_html
|
||||
. '</a>';
|
||||
}
|
||||
|
||||
// legacy: span.newadd-btns.hover-show > toggle chip + .hover-show-con.drop-newadd
|
||||
// Paint classes (jb-*, but, radius) MUST live on the toggle — wrapper is layout-only
|
||||
// (background forced transparent). Bare navbar-but has no .navbar-but-cluster shell.
|
||||
// data-aksrt-newadd: JS may portal menu to body so chip width cannot track menu.
|
||||
$html = '<span class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' hover-show')) . '" data-aksrt-newadd="1">';
|
||||
$html .= '<span class="newadd-btns__toggle navbar-cta ' . esc_attr($cta_class) . '">' . $label_html
|
||||
. '<i class="fa fas fa-caret-down" aria-hidden="true"></i></span>';
|
||||
$html .= '<div class="hover-show-con dropdown-menu drop-newadd" role="menu">';
|
||||
foreach ($actions as $a) {
|
||||
$html .= '<a rel="nofollow" class="btn-newadd newadd-btns__item" role="menuitem" href="' . esc_url($a['url']) . '">';
|
||||
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
|
||||
. '<text>' . esc_html($a['label']) . '</text>';
|
||||
$html .= '</a>';
|
||||
}
|
||||
$html .= '</div></span>';
|
||||
return $html;
|
||||
return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
|
||||
. $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
||||
*/
|
||||
function aksrt_render_header_menu_buttons(): string {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
||||
$cfg = [
|
||||
'enabled' => !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']),
|
||||
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))),
|
||||
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']),
|
||||
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
|
||||
];
|
||||
$enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
|
||||
$text = trim((string) ($settings['nav_newposts_text'] ?? ''));
|
||||
$pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
|
||||
$skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
|
||||
|
||||
$html = '';
|
||||
if (!empty($cfg['enabled'])) {
|
||||
if ($enabled) {
|
||||
// legacy default: but nowave jb-blue (+ radius).
|
||||
$class = 'but nowave navbar-newposts';
|
||||
$skin = trim((string) ($cfg['class'] ?? ''));
|
||||
if ($skin === '') {
|
||||
$skin = 'jb-blue';
|
||||
}
|
||||
$class .= ' ' . $skin;
|
||||
$pill = !empty($cfg['radius_pill']);
|
||||
$class = 'but nowave ' . $skin;
|
||||
if ($pill && !str_contains($class, 'radius')) {
|
||||
$class .= ' radius';
|
||||
}
|
||||
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt'));
|
||||
$label = $text !== '' ? $text : __('发布', 'aksrt');
|
||||
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
||||
if ($btn !== '') {
|
||||
$html .= $btn;
|
||||
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
||||
$show_vip = false;
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
||||
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip']);
|
||||
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip'])
|
||||
&& !\AKSRT\Settings\Settings::close_sign();
|
||||
}
|
||||
if ($show_vip) {
|
||||
$is_vip = false;
|
||||
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
||||
if (method_exists($svc, 'render_vip_card')) {
|
||||
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
||||
if ($cards !== '') {
|
||||
$hover = '<div class="hover-show-con sub-menu sub-vip-card">' . $cards . '</div>';
|
||||
$hover = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
// legacy: span.hover-show wraps button + .hover-show-con.sub-vip-card
|
||||
$html .= '<span class="hover-show inline-block navbar-vip-hover">';
|
||||
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"'
|
||||
. ($user_id > 0 ? ' vip-level="1"' : '') . '>';
|
||||
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
|
||||
$html .= '</a>' . $hover . '</span>';
|
||||
// legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
|
||||
$html .= '<span class="hover-show inline-block">';
|
||||
$html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
|
||||
. '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||
. esc_html__('开通会员', 'aksrt') . '</a>' . $hover . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
||||
* Check-in CTA for header dropdown and compact card placements.
|
||||
*
|
||||
* @param int $user_id Current user ID.
|
||||
* @param bool $compact Render a short card-friendly label.
|
||||
* @param string|null $class Override the classes and render the reference-parity
|
||||
* <a> badge (user-card 签到书签); null keeps the header
|
||||
* dropdown <button> runtime.
|
||||
* dropdown <a> runtime.
|
||||
*/
|
||||
function aksrt_header_user_checkin_btn(int $user_id = 0, bool $compact = false, ?string $class = null): string {
|
||||
function aksrt_header_user_checkin_btn(int $user_id = 0, ?string $class = null): string {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
||||
return '';
|
||||
@@ -56,38 +55,39 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
||||
|
||||
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
||||
if ($signed) {
|
||||
$icon = '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>';
|
||||
if ($class !== null) {
|
||||
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
||||
// 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
|
||||
return aksrt_get_user_checkin_details_link(
|
||||
$class . ' is-done',
|
||||
$icon . '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
||||
$class,
|
||||
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||
. esc_html__('已签到', 'aksrt')
|
||||
);
|
||||
}
|
||||
return aksrt_get_user_checkin_details_link(
|
||||
'but block c-yellow aksrt-header-checkin is-done',
|
||||
'<i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt'))
|
||||
'but block c-yellow is-done',
|
||||
'<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||
. esc_html__('今日已签到', 'aksrt')
|
||||
);
|
||||
}
|
||||
|
||||
$classes = $class ?? ('but block c-yellow aksrt-header-checkin' . ($compact ? ' aksrt-header-checkin--compact' : ''));
|
||||
|
||||
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
||||
// reference parity renders an <a> so the badge inherits the card font
|
||||
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
||||
if ($class !== null) {
|
||||
return '<a class="' . esc_attr($classes) . '" href="javascript:;" data-signin-btn'
|
||||
return '<a class="' . esc_attr($class) . '" href="javascript:;" data-signin-btn'
|
||||
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
||||
. '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||
. '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
||||
. '</a>';
|
||||
}
|
||||
|
||||
return '<button type="button" class="' . esc_attr($classes) . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
||||
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
|
||||
. '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>'
|
||||
. '</button>';
|
||||
return '<a class="but block c-yellow" href="javascript:;" data-signin-btn'
|
||||
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
|
||||
. '<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||
. '<span data-signin-label>' . esc_html__('签到领取今日奖励', 'aksrt') . '</span>'
|
||||
. '</a>';
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -118,41 +118,65 @@ add_action('wp_footer', 'aksrt_render_signin_captcha');
|
||||
|
||||
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
||||
/**
|
||||
* Mini wallet strip: credits + cash balance.
|
||||
* Mini wallet tiles: cash balance + credits (reference parity).
|
||||
*
|
||||
* Each tile is gated by its payment switch; an all-off configuration
|
||||
* renders nothing at all.
|
||||
*/
|
||||
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||||
return '';
|
||||
}
|
||||
$credits = aksrt_user_points($user_id);
|
||||
$currency = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_currency_name()
|
||||
: __('积分', 'aksrt');
|
||||
$wallet = aksrt_user_balance_decimal($user_id);
|
||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||
|
||||
$html = '<a class="aksrt-header-wallet flex ac jsb" href="' . esc_url($center) . '">';
|
||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-database" aria-hidden="true"></i> '
|
||||
. esc_html($currency) . ' <b data-aksrt-points-value data-credits-value>' . esc_html(number_format_i18n($credits)) . '</b></span>';
|
||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-money-bill" aria-hidden="true"></i> '
|
||||
. esc_html__('余额', 'aksrt') . ' <b data-aksrt-wallet-value>' . esc_html(number_format_i18n($wallet, 2)) . '</b></span>';
|
||||
$html .= '</a>';
|
||||
return $html;
|
||||
$balance_on = !empty(\AKSRT\Settings\Settings::get_payment_recharge_settings()['enabled']);
|
||||
$points_on = \AKSRT\Settings\Settings::credits_enabled();
|
||||
if (!$balance_on && !$points_on) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||
$box = '';
|
||||
$balance_icon = aksrt_icon_markup('zibsvg-money-color-2', 'em14');
|
||||
$points_icon = aksrt_icon_markup('zibsvg-points-color', 'em14');
|
||||
|
||||
if ($balance_on) {
|
||||
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||
. '<div class="muted-2-color px12">' . esc_html__('余额', 'aksrt')
|
||||
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||
. '<div class="flex jsb"><span class="font-bold c-blue-2" data-aksrt-wallet-value data-aksrt-count-format="cut-count">'
|
||||
. esc_html(aksrt_cut_count((float) aksrt_user_balance_decimal($user_id)))
|
||||
. '</span>' . $balance_icon . '</div></a>';
|
||||
}
|
||||
|
||||
if ($points_on) {
|
||||
$currency = \AKSRT\Settings\Settings::get_currency_name();
|
||||
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||
. '<div class="muted-2-color px12">' . esc_html($currency)
|
||||
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||
. '<div class="flex jsb"><span class="font-bold c-yellow" data-aksrt-points-value data-aksrt-count-format="cut-count">'
|
||||
. esc_html(aksrt_cut_count(aksrt_user_points($user_id)))
|
||||
. '</span>' . $points_icon . '</div></a>';
|
||||
}
|
||||
|
||||
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_header_user_badges_row')) :
|
||||
/**
|
||||
* Identity / medal chips under profile.
|
||||
* Interaction stats row under the wallet (reference parity).
|
||||
*/
|
||||
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
if ($user_id <= 0 || !function_exists('aksrt_render_user_identity_chips')) {
|
||||
if ($user_id <= 0 || !function_exists('aksrt_get_user_badges')) {
|
||||
return '';
|
||||
}
|
||||
// Certification is displayed beside the name and level icon in the profile row.
|
||||
return '';
|
||||
$items = aksrt_get_user_badges($user_id);
|
||||
if ($items === '') {
|
||||
return '';
|
||||
}
|
||||
return '<div class="em09 author-tag mb10 mt6 flex jc">' . $items . '</div>';
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -173,30 +197,33 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
||||
return '';
|
||||
}
|
||||
|
||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_all()
|
||||
: [];
|
||||
|
||||
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
||||
if ($level > 0) {
|
||||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||||
$name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||
$name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||
// 到期徽章语义对齐参考主题 zib_get_user_vip_exp_date_text():只有真的拿到
|
||||
// 到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;显式永久
|
||||
// 会员显示「永久会员」;拿不到到期数据时参考主题返回 false,整枚徽章不渲染。
|
||||
$exp_label = '';
|
||||
if (function_exists('aksrt_get_vip_info')) {
|
||||
$info = aksrt_get_vip_info($user_id);
|
||||
$expires = max(0, (int) ($info['expires'] ?? 0));
|
||||
$exp_label = $expires > 0
|
||||
? sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires))
|
||||
: (string) ($info['expires_label'] ?? '');
|
||||
if (function_exists('aksrt_get_user_vip_state')) {
|
||||
$state = aksrt_get_user_vip_state($user_id);
|
||||
$expires = max(0, (int) ($state['expires'] ?? 0));
|
||||
if ($expires > 0) {
|
||||
$exp_label = $expires <= time()
|
||||
? __('会员已过期', 'aksrt')
|
||||
: wp_date('Y年m月d日', $expires);
|
||||
} elseif (!empty($state['is_permanent'])) {
|
||||
$exp_label = __('永久会员', 'aksrt');
|
||||
}
|
||||
}
|
||||
$html = '<div class="mt10 em09 aksrt-header-payvip is-vip">';
|
||||
$html .= '<span class="radius4 payvip-icon btn-block text-center vipbg-v2 level-' . (int) $level . ' aksrt-header-payvip__active">';
|
||||
$html .= '<span class="aksrt-header-payvip__level">';
|
||||
$html .= '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
||||
$html .= '<span>' . esc_html($name) . '</span>';
|
||||
$html .= '</span>';
|
||||
$html = '<div class="mt10 em09" style="padding:2px;">';
|
||||
$html .= '<span class="radius4 payvip-icon btn-block text-center">';
|
||||
$html .= function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id, 'em12 mr6', false) : '';
|
||||
$html .= '<span>' . wp_kses_post($name) . '</span>';
|
||||
if ($exp_label !== '') {
|
||||
$html .= '<span class="aksrt-header-payvip__exp">' . esc_html($exp_label) . '</span>';
|
||||
$html .= '<span class="ml10 badg jb-yellow vip-expdate-tag">'
|
||||
. '<i class="fa fas fa-clock mr3" aria-hidden="true"></i>'
|
||||
. esc_html($exp_label) . '</span>';
|
||||
}
|
||||
$html .= '</span></div>';
|
||||
return $html;
|
||||
@@ -205,11 +232,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
||||
return '';
|
||||
}
|
||||
|
||||
// Match check-in CTA geometry (full width, 6px radius) — not top-nav navbar-cta chips.
|
||||
return '<div class="mt10 em09 aksrt-header-payvip">'
|
||||
. '<a class="pay-vip but block payvip-icon aksrt-header-payvip__cta" href="javascript:;" vip-level="1">'
|
||||
. '<i class="fa fas fa-gem" aria-hidden="true"></i>'
|
||||
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>'
|
||||
// 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
|
||||
return '<div class="mt10 em09" style="padding:2px;">'
|
||||
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
|
||||
. '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||
. esc_html__('开通会员 尊享会员权益', 'aksrt')
|
||||
. '</a></div>';
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -42,7 +42,7 @@ endif;
|
||||
|
||||
if (!function_exists('aksrt_brand_icon_map')) :
|
||||
/**
|
||||
* Short key → FA5 class string (allowlisted).
|
||||
* Short key → FA5 class string or built-in SVG sprite key (allowlisted).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
|
||||
return $map;
|
||||
}
|
||||
|
||||
// FA5 Free has no bilibili/gitee brands — use solid stand-ins.
|
||||
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
|
||||
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
|
||||
// a solid stand-in; every other key stays on FA5.
|
||||
$map = [
|
||||
'github' => 'fa fab fa-github',
|
||||
'google' => 'fa fab fa-google',
|
||||
'google' => 'zibsvg-google-color',
|
||||
'x' => 'fa fab fa-twitter',
|
||||
'twitter' => 'fa fab fa-twitter',
|
||||
'email' => 'fa fas fa-envelope',
|
||||
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
|
||||
'instagram' => 'fa fab fa-instagram',
|
||||
'web' => 'fa fas fa-globe',
|
||||
'globe' => 'fa fas fa-globe',
|
||||
'gitee' => 'fa fas fa-code-branch',
|
||||
'gitee' => 'zibsvg-gitee',
|
||||
'baidu' => 'fa fas fa-paw',
|
||||
'alipay' => 'fa fab fa-alipay',
|
||||
];
|
||||
@@ -127,10 +129,10 @@ endif;
|
||||
|
||||
if (!function_exists('aksrt_brand_icon_class')) :
|
||||
/**
|
||||
* Resolve a short key or FA class string to an allowlisted FA5 class.
|
||||
* Resolve a short key, FA class string or sprite key to its icon value.
|
||||
*
|
||||
* @param string $name Short key or full FA class string.
|
||||
* @return string e.g. 'fa fab fa-github', or '' if unknown.
|
||||
* @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
|
||||
* @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
|
||||
*/
|
||||
function aksrt_brand_icon_class(string $name): string {
|
||||
$raw = trim($name);
|
||||
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Built-in SVG sprite and inline SVG values pass straight through; they are
|
||||
// not FA class strings and must survive to the markup renderer untouched.
|
||||
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
// Pass through already-valid full FA class strings (footer contact_more).
|
||||
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
|
||||
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
|
||||
@@ -164,14 +172,21 @@ endif;
|
||||
|
||||
if (!function_exists('aksrt_brand_icon')) :
|
||||
/**
|
||||
* Escaped FA <i> markup for a brand/contact icon.
|
||||
* Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
|
||||
*
|
||||
* @param string $name Short key or FA class string.
|
||||
* @param string $name Short key, FA class string or zibsvg-/customsvg- value.
|
||||
* @param array|string $extra_classes Optional extra classes.
|
||||
* @return string Safe HTML or empty string when unknown.
|
||||
*/
|
||||
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
||||
$class = aksrt_brand_icon_class($name);
|
||||
|
||||
// Sprite and inline-SVG values carry their own element, so the shared icon
|
||||
// renderer owns the markup (it emits <svg class="icon"><use …>).
|
||||
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
|
||||
return aksrt_icon_markup($class, $extra_classes);
|
||||
}
|
||||
|
||||
if ($class === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -351,14 +351,14 @@ if (!function_exists('aksrt_get_user_badges')) {
|
||||
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
||||
|
||||
$html = '';
|
||||
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('post') . $post_n . '</a>';
|
||||
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('comment') . $com_n . '</a>';
|
||||
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-file-alt" aria-hidden="true"></i>' . $post_n . '</a>';
|
||||
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-comment-dots" aria-hidden="true"></i>' . $com_n . '</a>';
|
||||
if ($followed_n) {
|
||||
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
||||
} elseif ($like_n) {
|
||||
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '">' . aksrt_renderer_svg_icon('like') . $like_n . '</span>';
|
||||
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '"><i class="fa fas fa-thumbs-up" aria-hidden="true"></i>' . $like_n . '</span>';
|
||||
}
|
||||
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '">' . aksrt_renderer_svg_icon('hot') . $view_n . '</span>';
|
||||
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '"><i class="fa fas fa-fire" aria-hidden="true"></i>' . $view_n . '</span>';
|
||||
|
||||
/**
|
||||
* Filter the author-card count badges row.
|
||||
|
||||
@@ -428,10 +428,7 @@ if (!function_exists('aksrt_submit_links_modal')) :
|
||||
$body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">'
|
||||
. '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>';
|
||||
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
||||
$social = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
||||
if ($social !== '') {
|
||||
$body .= '<div class="social_loginbar">' . $social . '</div>';
|
||||
}
|
||||
$body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
||||
}
|
||||
$body .= '</div>';
|
||||
} else {
|
||||
|
||||
@@ -1309,6 +1309,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
||||
}
|
||||
|
||||
$level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level));
|
||||
$level_name_html = (string) ($level_settings['name_html'] ?? $level_name);
|
||||
$price = $vip_minor === 0
|
||||
? esc_html__('免费', 'aksrt')
|
||||
: $mark . esc_html(aksrt_format_price($value));
|
||||
@@ -1319,7 +1320,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
||||
? ' data-toggle="tooltip" title="' . esc_attr($title) . '"'
|
||||
: '';
|
||||
|
||||
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . esc_html($level_name) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
||||
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . wp_kses_post($level_name_html) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
||||
}
|
||||
|
||||
$html = implode('', $items);
|
||||
@@ -1384,10 +1385,13 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
||||
$vip_action = $vip_level > 0
|
||||
? sprintf(__('升级%s', 'aksrt'), $level_name)
|
||||
: __('开通会员', 'aksrt');
|
||||
$vip_action_html = $vip_level > 0
|
||||
? __('升级', 'aksrt') . wp_kses_post((string) ($level_settings['name_html'] ?? $level_name))
|
||||
: esc_html__('开通会员', 'aksrt');
|
||||
$action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader';
|
||||
$icon = aksrt_get_pay_vip_icon_html($level_settings);
|
||||
$button = '<div class="badg c-yellow em09 mb6"><i class="fa fas fa-fw fa-info-circle mr6" aria-hidden="true"></i>' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '</div>';
|
||||
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . esc_html($vip_action) . '</a>';
|
||||
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . $vip_action_html . '</a>';
|
||||
} elseif (!is_user_logged_in()) {
|
||||
if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) {
|
||||
$button = '<button type="button" class="but jb-red padding-lg cashier-link' . $block_class . '" data-aksrt-pay-content="' . esc_attr((string) $post->ID) . '"><i class="fa fas fa-shopping-cart" aria-hidden="true"></i> ' . esc_html__('立即购买', 'aksrt') . '</button>';
|
||||
|
||||
@@ -285,11 +285,12 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
}
|
||||
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
||||
|
||||
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg');
|
||||
// 会员/禁封徽章不再挂头像角标,改由名字行的会员位出图(aksrt_get_user_name)。
|
||||
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg', true, false);
|
||||
$name_html = aksrt_get_user_name([
|
||||
'id' => $author_id,
|
||||
'class' => 'flex1 flex ac',
|
||||
'vip' => false,
|
||||
'vip' => true,
|
||||
'medal' => true,
|
||||
'auth' => true,
|
||||
'level' => true,
|
||||
|
||||
@@ -481,7 +481,7 @@ endif;
|
||||
* or Chinese keyword heuristics — that mangles intentional HTML.
|
||||
*
|
||||
* Rules:
|
||||
* - Sanitize with a small allowlist.
|
||||
* - Sanitize with WordPress post-safe HTML.
|
||||
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
||||
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
||||
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
||||
@@ -497,21 +497,7 @@ if (!function_exists('aksrt_format_vip_equity_html')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'li' => ['class' => true],
|
||||
'ul' => ['class' => true],
|
||||
'ol' => ['class' => true],
|
||||
'strong' => [],
|
||||
'b' => [],
|
||||
'em' => [],
|
||||
'i' => ['class' => true],
|
||||
'span' => ['class' => true],
|
||||
'br' => [],
|
||||
'p' => ['class' => true],
|
||||
'div' => ['class' => true],
|
||||
'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true],
|
||||
];
|
||||
$clean = trim(wp_kses($raw, $allowed));
|
||||
$clean = trim(wp_kses_post($raw));
|
||||
if ($clean === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -138,10 +138,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$con = '';
|
||||
|
||||
if ($user_id > 0) {
|
||||
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
||||
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
||||
$display_name = $user->display_name ?: $user->user_login;
|
||||
$avatar = aksrt_get_avatar_html($user_id, 44, ['alt' => $display_name]);
|
||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img', true, false);
|
||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
||||
// Level and certification icons ride inline to the right of the display name.
|
||||
$level_badge = (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html'))
|
||||
@@ -174,13 +174,13 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
|
||||
$msg_icon = '';
|
||||
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
||||
$msg_icon = '<a class="abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
||||
$msg_icon .= '<i class="fa fas fa-bell" aria-hidden="true"></i>';
|
||||
if ($unread > 0) {
|
||||
$msg_icon .= '<span class="aksrt-notification-badge">' . esc_html((string) min(99, $unread)) . '</span>';
|
||||
}
|
||||
$msg_icon .= '</a>';
|
||||
$msg_icon = '<a class="msg-news-icon abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
||||
$msg_icon .= '<span class="toggle-radius msg-icon"><i class="fa fas fa-bell" aria-hidden="true"></i></span>';
|
||||
if ($unread > 0) {
|
||||
$msg_icon .= '<badge class="top">' . esc_html((string) min(99, $unread)) . '</badge>';
|
||||
}
|
||||
$msg_icon .= '</a>';
|
||||
}
|
||||
|
||||
$href = '<div class="flex jsa header-user-href">';
|
||||
$href .= '<a rel="nofollow" href="' . esc_url(function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')) . '">';
|
||||
@@ -224,6 +224,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
||||
// legacy: 商城中心 → 支付中心(AKSRT 支付中心后台菜单)。
|
||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('admin.php?page=aksrt_pay')) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-credit-card" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-yellow">' . esc_html__('支付中心', 'aksrt') . '</div></a>';
|
||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
||||
@@ -234,7 +238,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$con .= $avatar;
|
||||
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
||||
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
||||
. '<span class="display-name text-ellipsis">' . esc_html($display_name) . '</span>' . $vip_badge . $medal_badge . $certification_badge . $level_badge
|
||||
. '<name class="flex ac flex1">'
|
||||
. '<a class="display-name text-ellipsis" href="' . esc_url(function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id)) . '">' . esc_html($display_name) . '</a>'
|
||||
. $vip_badge . $medal_badge . $certification_badge . $level_badge
|
||||
. '</name>'
|
||||
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
||||
$con .= '</div>';
|
||||
$con .= $msg_icon;
|
||||
@@ -264,17 +271,22 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
||||
. esc_html__('注册', 'aksrt') . '</div></a>';
|
||||
}
|
||||
$href .= '<a rel="nofollow" href="' . esc_url(wp_lostpassword_url(home_url('/'))) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
||||
// legacy: 找回密码 → 统一登录页 resetpassword 标签(带回跳)。
|
||||
$reset_url = function_exists('aksrt_get_sign_page_url')
|
||||
? aksrt_get_sign_page_url('resetpassword', [
|
||||
'redirect_to' => remove_query_arg(
|
||||
'redirect_to',
|
||||
home_url(isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '/')
|
||||
),
|
||||
])
|
||||
: wp_lostpassword_url(home_url('/'));
|
||||
$href .= '<a target="_blank" rel="nofollow" href="' . esc_url($reset_url) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
||||
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
||||
$href .= '</div>';
|
||||
|
||||
$con = '<div class="text-center">' . $href . '</div>';
|
||||
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
||||
$oauth_html = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
|
||||
if ($oauth_html !== '') {
|
||||
$con .= '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html__('快速登录', 'aksrt') . '</p>';
|
||||
$con .= '<div class="social_loginbar">' . $oauth_html . '</div>';
|
||||
}
|
||||
$con .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +299,12 @@ if (!function_exists('aksrt_menu_button')) :
|
||||
/**
|
||||
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
||||
*
|
||||
* The reference splits the cluster into three floated .navbar-form
|
||||
* groups (publish/VIP, theme + language, account row). Each group
|
||||
* keeps its own .navbar-form padding, which is what produces the
|
||||
* reference inter-cluster spacing, so the groups are emitted
|
||||
* separately instead of being merged into one flex container.
|
||||
*
|
||||
* @param array<string,mixed> $ctx
|
||||
*/
|
||||
function aksrt_menu_button(array $ctx): void {
|
||||
@@ -355,14 +373,24 @@ if (!function_exists('aksrt_menu_button')) :
|
||||
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
||||
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||
. $notification_but . $sign_but . $search_but . '</ul>';
|
||||
$button = '<div class="aksrt-navbar-actions navbar-form navbar-right navbar-but">'
|
||||
. $actions . $utility_buttons . $account . '</div>';
|
||||
|
||||
// Reference cluster split: publish/VIP, theme + language, account row.
|
||||
$button = '';
|
||||
if ($actions !== '') {
|
||||
$button = '<div class="navbar-form navbar-right navbar-but">' . $actions . '</div>';
|
||||
}
|
||||
if ($utility_buttons !== '') {
|
||||
$button .= '<div class="navbar-form navbar-right flex ac">' . $utility_buttons . '</div>';
|
||||
}
|
||||
$right_but = '<div class="navbar-form navbar-right'
|
||||
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||
. $account . '</div>';
|
||||
|
||||
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
||||
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
||||
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
||||
|
||||
$html = $fold . $button;
|
||||
$html = $fold . $button . $right_but;
|
||||
|
||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
/**
|
||||
* Affiliate poster preview modal.
|
||||
*
|
||||
* The 推广海报 button lives in the revenue detail link row; the QR code is no
|
||||
* longer rendered on its own — it exists only inside the generated poster.
|
||||
* 1:1 replica of the reference theme's poster-share modal (zibll js/poster-share.js
|
||||
* + css/main.css `.poster-share` block): a borderless 360px Bootstrap-style dialog
|
||||
* centred by `.modal.flex.jc`, a `.loading-mask` overlay while the canvas renders,
|
||||
* and circular download/close buttons on a `.hover-show-con` layer that only fades
|
||||
* in on hover. The 推广海报 button lives in the revenue detail link row; the QR code
|
||||
* is no longer rendered on its own — it exists only inside the generated poster.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -15,14 +19,18 @@ if (!empty($GLOBALS['aksrt_affiliate_poster_modal_rendered'])) {
|
||||
}
|
||||
$GLOBALS['aksrt_affiliate_poster_modal_rendered'] = true;
|
||||
?>
|
||||
<div class="uc-modal" id="aksrt-affiliate-poster-modal" hidden data-aksrt-affiliate-poster-modal>
|
||||
<div class="uc-modal__overlay" data-affiliate-poster-close></div>
|
||||
<div class="uc-modal__dialog" role="dialog" aria-modal="true">
|
||||
<button type="button" class="uc-modal__close" data-affiliate-poster-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
|
||||
<h3 class="uc-modal__title"><?php esc_html_e('推广海报', 'aksrt'); ?></h3>
|
||||
<div class="uc-affiliate-poster" data-affiliate-poster-stage>
|
||||
<p class="muted-2-color em09"><?php esc_html_e('正在生成海报…', 'aksrt'); ?></p>
|
||||
<div class="modal fade flex jc" id="aksrt-affiliate-poster-modal" data-aksrt-affiliate-poster-modal tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog poster-share hover-show" role="document">
|
||||
<div class="modal-content" style="height:220px;overflow:hidden">
|
||||
<div class="poster-imgbox hover-show" data-affiliate-poster-stage></div>
|
||||
<div class="flex jc loading-mask absolute main-bg radius8" data-affiliate-poster-loading>
|
||||
<div class="loading zts fa-2x"></div>
|
||||
<div class="muted-2-color box-body"><?php esc_html_e('正在生成图片,请稍候...', 'aksrt'); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padding-6 abs-center hover-show-con text-center" style="top:auto">
|
||||
<a href="javascript:;" class="but cir jb-blue poster-download hide" data-affiliate-poster-download aria-label="<?php esc_attr_e('保存海报', 'aksrt'); ?>"><i class="fas fa-download" aria-hidden="true"></i></a>
|
||||
<button type="button" data-dismiss="modal" class="but cir jb-yellow ml10" aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><svg aria-hidden="true" class="icon em12"><use xlink:href="#icon-close"></use></svg></button>
|
||||
</div>
|
||||
<button type="button" class="but jb-blue btn-block mt10" data-affiliate-poster-download hidden><?php esc_html_e('保存海报', 'aksrt'); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
<?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>
|
||||
@@ -121,7 +121,6 @@ $status_labels = [
|
||||
$pool_amount = $format_amount($commission_stats['pool'] ?? 0);
|
||||
$total_amount = $format_amount($commission_stats['total'] ?? 0);
|
||||
$withdrawn_amount = $format_amount($commission_stats['withdrawn'] ?? 0);
|
||||
$debt_amount = $format_amount($commission_stats['debt'] ?? 0);
|
||||
$reward_panes = ['detail'];
|
||||
if ($affiliate_on) {
|
||||
$reward_panes[] = 'orders';
|
||||
@@ -148,7 +147,6 @@ $pane_is_active = static function (string $tab) use ($active_reward_tab): bool {
|
||||
<p class="uc-commission-card__meta">
|
||||
<span><?php printf(esc_html__('累计佣金 ¥%s', 'aksrt'), esc_html($total_amount)); ?></span>
|
||||
<span><?php printf(esc_html__('已提现 ¥%s', 'aksrt'), esc_html($withdrawn_amount)); ?></span>
|
||||
<?php if (($commission_stats['debt'] ?? '0.00') !== '0.00') : ?><span class="uc-commission-card__debt"><?php printf(esc_html__('待偿欠款 ¥%s', 'aksrt'), esc_html($debt_amount)); ?></span><?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php if ($withdraw_on) : ?>
|
||||
@@ -178,7 +176,7 @@ $pane_is_active = static function (string $tab) use ($active_reward_tab): bool {
|
||||
</div>
|
||||
<?php if ($affiliate_on) : ?>
|
||||
<div class="uc-revenue-summary-row"><span><?php esc_html_e('佣金比例', 'aksrt'); ?></span><strong data-affiliate-ratio><?php echo esc_html((string) $commission_ratio); ?>%</strong></div>
|
||||
<div class="uc-revenue-summary-row uc-revenue-summary-row--total"><span><?php esc_html_e('累计佣金', 'aksrt'); ?></span><div><strong><?php printf(esc_html__('累计 %s 元', 'aksrt'), esc_html($total_amount)); ?></strong><strong><?php printf(esc_html__('已提现 %s 元', 'aksrt'), esc_html($withdrawn_amount)); ?></strong><?php if (($commission_stats['debt'] ?? '0.00') !== '0.00') : ?><strong><?php printf(esc_html__('待偿欠款 %s 元', 'aksrt'), esc_html($debt_amount)); ?></strong><?php endif; ?></div></div>
|
||||
<div class="uc-revenue-summary-row uc-revenue-summary-row--total"><span><?php esc_html_e('累计佣金', 'aksrt'); ?></span><div><strong><?php printf(esc_html__('累计 %s 元', 'aksrt'), esc_html($total_amount)); ?></strong><strong><?php printf(esc_html__('已提现 %s 元', 'aksrt'), esc_html($withdrawn_amount)); ?></strong></div></div>
|
||||
<div class="uc-revenue-summary-row uc-revenue-summary-row--explain"><span><?php echo esc_html($details_title); ?></span><div class="uc-reward-richtext"><?php echo wp_kses_post(wpautop($details_text)); ?></div></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,8 @@ require __DIR__ . '/sidebar-vip.php';
|
||||
require __DIR__ . '/sidebar-commerce.php';
|
||||
require __DIR__ . '/sidebar-creator.php';
|
||||
require __DIR__ . '/sidebar-nav.php';
|
||||
$drawer_trigger_icon = aksrt_icon_markup('zibsvg-menu');
|
||||
?>
|
||||
<button type="button" class="uc-sidebar__drawer-trigger" id="uc-drawer-trigger" aria-label="<?php esc_attr_e('打开导航', 'aksrt'); ?>">
|
||||
<i class="fa fas fa-th-large" aria-hidden="true"></i>
|
||||
<?php echo $drawer_trigger_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</button>
|
||||
@@ -26,10 +26,14 @@ foreach ((array) ($oauth_data['bindings'] ?? []) as $binding) {
|
||||
if (!$key) {
|
||||
continue;
|
||||
}
|
||||
// Reference parity: the provider type (clogin_ prefix stripped) drives the
|
||||
// .social-login-item brand colour, matching zibll's binding list.
|
||||
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||
$binding_rows[$key] = [
|
||||
'key' => $key,
|
||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||
'icon' => $binding['icon'] ?? '',
|
||||
'icon_type' => $icon_type,
|
||||
'is_bound' => true,
|
||||
'url' => '',
|
||||
];
|
||||
@@ -39,10 +43,12 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
||||
if (!$key) {
|
||||
continue;
|
||||
}
|
||||
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||
$binding_rows[$key] = [
|
||||
'key' => $key,
|
||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||
'icon' => $binding['icon'] ?? '',
|
||||
'icon_type' => $icon_type,
|
||||
'is_bound' => false,
|
||||
'url' => $binding['start_url'] ?? $binding['url'] ?? '',
|
||||
];
|
||||
@@ -64,7 +70,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
||||
<?php foreach ($binding_rows as $binding) : ?>
|
||||
<?php if (!empty($binding['is_bound'])) : ?>
|
||||
<div class="aksrt-account-row binding-item">
|
||||
<span class="binding-item__icon"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="social-login-item circular <?php echo esc_attr($binding['icon_type']); ?>"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||
<span class="binding-status">
|
||||
<i class="fa fas fa-check-circle badge__icon" aria-hidden="true"></i>
|
||||
@@ -76,7 +82,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<a href="<?php echo esc_url($binding['url']); ?>" class="aksrt-account-row binding-item binding-item--linkable" target="_self" data-provider="<?php echo esc_attr($binding['key']); ?>" data-bind-start-url="<?php echo esc_url($binding['url']); ?>">
|
||||
<span class="binding-item__icon"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="social-login-item circular <?php echo esc_attr($binding['icon_type']); ?>"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||
<span class="binding-status binding-status--idle"><?php esc_html_e('未绑定', 'aksrt'); ?></span>
|
||||
<span class="but hollow c-blue aksrt-account-row__action"><?php esc_html_e('绑定', 'aksrt'); ?></span>
|
||||
|
||||
@@ -20,23 +20,26 @@ try {
|
||||
$wallet = '0.00';
|
||||
}
|
||||
$points = max(0, (int) ($credit_balance ?? 0));
|
||||
$wallet_icon = aksrt_icon_markup('zibsvg-money-color-2');
|
||||
$points_icon = aksrt_icon_markup('zibsvg-points-color');
|
||||
$arrow_icon = aksrt_icon_markup('zibsvg-right');
|
||||
?>
|
||||
<div class="uc-sidebar__pair uc-sidebar__pair--assets">
|
||||
<?php if ($show_wallet) : ?>
|
||||
<a class="uc-summary-card uc-summary-card--wallet" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
||||
<span class="uc-summary-card__head"><?php esc_html_e('账户余额', 'aksrt'); ?><i class="fa fas fa-angle-right" aria-hidden="true"></i></span>
|
||||
<span class="uc-summary-card__head"><?php esc_html_e('账户余额', 'aksrt'); ?><?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="uc-summary-card__body">
|
||||
<strong data-aksrt-wallet-value><?php echo esc_html($wallet); ?></strong>
|
||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
||||
<?php echo $wallet_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($show_credits) : ?>
|
||||
<a class="uc-summary-card uc-summary-card--points" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
||||
<span class="uc-summary-card__head"><?php echo esc_html((string) ($currency_name ?? __('积分', 'aksrt'))); ?><i class="fa fas fa-angle-right" aria-hidden="true"></i></span>
|
||||
<span class="uc-summary-card__head"><?php echo esc_html((string) ($currency_name ?? __('积分', 'aksrt'))); ?><?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="uc-summary-card__body">
|
||||
<strong data-aksrt-points-value data-credits-value><?php echo esc_html(aksrt_format_points($points)); ?></strong>
|
||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
||||
<?php echo $points_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -13,20 +13,23 @@ $revenue_url = isset($dashboard_tabs['revenue']) ? aksrt_get_user_center_tab_url
|
||||
if ($creation_url === '' && $revenue_url === '') {
|
||||
return;
|
||||
}
|
||||
$creation_icon = aksrt_icon_markup('zibsvg-merchant-color');
|
||||
$revenue_icon = aksrt_icon_markup('zibsvg-money-color');
|
||||
$arrow_icon = aksrt_icon_markup('zibsvg-right');
|
||||
?>
|
||||
<div class="uc-sidebar__pair uc-sidebar__pair--centers">
|
||||
<?php if ($creation_url !== '') : ?>
|
||||
<a class="uc-center-card uc-center-card--creation" href="<?php echo esc_url($creation_url); ?>" data-tab="creator">
|
||||
<span class="uc-center-card__icon"><i class="fa fas fa-pen" aria-hidden="true"></i></span>
|
||||
<span class="uc-center-card__icon"><?php echo $creation_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span><strong><?php esc_html_e('创作收益', 'aksrt'); ?></strong><small><?php esc_html_e('作者收益明细', 'aksrt'); ?></small></span>
|
||||
<i class="fa fas fa-angle-right" aria-hidden="true"></i>
|
||||
<?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($revenue_url !== '') : ?>
|
||||
<a class="uc-center-card uc-center-card--revenue" href="<?php echo esc_url($revenue_url); ?>" data-tab="revenue">
|
||||
<span class="uc-center-card__icon"><i class="fa fas fa-chart-line" aria-hidden="true"></i></span>
|
||||
<span class="uc-center-card__icon"><?php echo $revenue_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span><strong><?php esc_html_e('推广收益', 'aksrt'); ?></strong><small><?php esc_html_e('推广佣金明细', 'aksrt'); ?></small></span>
|
||||
<i class="fa fas fa-angle-right" aria-hidden="true"></i>
|
||||
<?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -2,6 +2,9 @@
|
||||
/**
|
||||
* User center sidebar: services and account settings.
|
||||
*
|
||||
* Icon set mirrors the reference theme user-center sidebar: every entry renders
|
||||
* an inline SVG from the theme sprite through aksrt_icon_markup().
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -17,22 +20,20 @@ $orders_url = $orders_available ? (string) ($orders_url ?? '') : '';
|
||||
$services = array_values(array_filter([
|
||||
[
|
||||
'label' => __('我的等级', 'aksrt'),
|
||||
'icon' => 'chart-line',
|
||||
'tone' => 'level',
|
||||
'icon' => 'trend-color',
|
||||
'url' => $level_url,
|
||||
'tab' => 'level',
|
||||
],
|
||||
|
||||
[
|
||||
'label' => __('官方认证', 'aksrt'),
|
||||
'icon' => 'id-badge',
|
||||
'tone' => 'auth',
|
||||
'icon' => 'user-auth',
|
||||
'url' => $auth_url,
|
||||
'tab' => 'auth',
|
||||
],
|
||||
[
|
||||
'label' => __('我的订单', 'aksrt'),
|
||||
'icon' => 'receipt',
|
||||
'tone' => 'orders',
|
||||
'icon' => 'order-color',
|
||||
'url' => $orders_url,
|
||||
'tab' => 'orders',
|
||||
],
|
||||
@@ -41,35 +42,38 @@ $services = array_values(array_filter([
|
||||
$settings = array_values(array_filter([
|
||||
[
|
||||
'label' => __('消息通知', 'aksrt'),
|
||||
'icon' => 'bell',
|
||||
'tone' => 'notice',
|
||||
'icon' => 'msg-color',
|
||||
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
||||
'tab' => 'msg',
|
||||
],
|
||||
[
|
||||
'label' => __('个人资料', 'aksrt'),
|
||||
'icon' => 'user-edit',
|
||||
'tone' => 'profile',
|
||||
'icon' => 'user-color',
|
||||
'url' => isset($dashboard_tabs['profile']) ? aksrt_get_user_center_tab_url('profile') : '',
|
||||
'tab' => 'profile',
|
||||
],
|
||||
[
|
||||
'label' => __('账户安全', 'aksrt'),
|
||||
'icon' => 'shield-alt',
|
||||
'tone' => 'security',
|
||||
'icon' => 'security-color',
|
||||
'url' => isset($dashboard_tabs['settings']) ? aksrt_get_user_center_tab_url('settings') : '',
|
||||
'tab' => 'settings',
|
||||
],
|
||||
], static fn(array $item): bool => (string) $item['url'] !== ''));
|
||||
|
||||
$render_grid = static function (array $items): void {
|
||||
?>
|
||||
<div class="uc-service-grid">
|
||||
<?php foreach ($items as $item) : ?>
|
||||
<a class="uc-service-grid__item uc-service-grid__item--<?php echo esc_attr((string) $item['tone']); ?><?php echo !empty($item['tab']) && ($active_tab ?? '') === $item['tab'] ? ' is-active' : ''; ?>" href="<?php echo esc_url((string) $item['url']); ?>"<?php if (!empty($item['tab'])) : ?> data-tab="<?php echo esc_attr((string) $item['tab']); ?>"<?php endif; ?>>
|
||||
<span class="uc-service-grid__icon">
|
||||
<i class="fa fas fa-<?php echo esc_attr((string) $item['icon']); ?>" aria-hidden="true"></i>
|
||||
<?php if (!empty($item['count'])) : ?><small><?php echo esc_html((string) $item['count']); ?></small><?php endif; ?>
|
||||
</span>
|
||||
<span><?php echo esc_html((string) $item['label']); ?></span>
|
||||
<div class="flex ac hh text-center icon-but-box user-icon-but-box">
|
||||
<?php foreach ($items as $item) :
|
||||
$tab = (string) ($item['tab'] ?? '');
|
||||
$item_class = 'icon-but-' . ($tab !== '' ? $tab : 'item');
|
||||
if (($active_tab ?? '') === $tab) {
|
||||
$item_class .= ' is-active';
|
||||
}
|
||||
$icon_html = aksrt_icon_markup('zibsvg-' . (string) $item['icon']);
|
||||
?>
|
||||
<a class="<?php echo esc_attr($item_class); ?>" href="<?php echo esc_url((string) $item['url']); ?>"<?php if ($tab !== '') : ?> data-tab="<?php echo esc_attr($tab); ?>"<?php endif; ?>>
|
||||
<div class="em16"><?php echo $icon_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||
<div class="px12 muted-color mt3"><?php echo esc_html((string) $item['label']); ?></div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ if ($status === '') {
|
||||
}
|
||||
|
||||
if ($vip_level > 0) {
|
||||
$title = (string) ($vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||
$title = (string) ($vip_levels[$vip_level]['name_html'] ?? $vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||
if ($expires <= 0 && ($vip_state['source'] ?? 'none') !== 'order') {
|
||||
$status = __('永久会员', 'aksrt');
|
||||
} else {
|
||||
@@ -41,7 +41,7 @@ if ($vip_level > 0) {
|
||||
);
|
||||
}
|
||||
} elseif ($expired_level > 0) {
|
||||
$title = (string) ($vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$title = (string) ($vip_levels[$expired_level]['name_html'] ?? $vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$status = __('已过期', 'aksrt');
|
||||
}
|
||||
|
||||
@@ -49,13 +49,14 @@ $sidebar_level = $vip_level > 0 ? $vip_level : $expired_level;
|
||||
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
||||
$sidebar_icon = $sidebar_icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $sidebar_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||
: '<i class="fa fas fa-certificate" aria-hidden="true"></i>';
|
||||
: aksrt_icon_markup('zibsvg-vip_1');
|
||||
$sidebar_arrow = aksrt_icon_markup('zibsvg-right', 'uc-sidebar-vip__arrow');
|
||||
?>
|
||||
<a class="uc-sidebar-vip" href="<?php echo esc_url($vip_url); ?>" data-tab="vip">
|
||||
<span class="uc-sidebar-vip__icon"><?php echo $sidebar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="uc-sidebar-vip__content">
|
||||
<strong><?php echo esc_html($title); ?></strong>
|
||||
<small><?php echo esc_html($status); ?></small>
|
||||
<strong><?php echo wp_kses_post($title); ?></strong>
|
||||
<small><?php echo wp_kses_post($status); ?></small>
|
||||
</span>
|
||||
<i class="fa fas fa-angle-right uc-sidebar-vip__arrow" aria-hidden="true"></i>
|
||||
<?php echo $sidebar_arrow; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
@@ -29,6 +29,8 @@ $vip_state_name = (string) ($vip_state['state'] ?? 'unopened');
|
||||
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
||||
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
||||
$desc_top = (string) ($vip_settings['desc'] ?? '');
|
||||
$renew_desc = (string) ($vip_settings['renew']['desc'] ?? '');
|
||||
$upgrade_desc = (string) ($vip_settings['upgrade']['desc'] ?? '');
|
||||
|
||||
// Membership summary: title → level → intro → expiry.
|
||||
$vip_state = 'unopened';
|
||||
@@ -36,13 +38,15 @@ $vip_state_label = __('未开通', 'aksrt');
|
||||
$vip_level_number = 0;
|
||||
$vip_level_name = __('未开通会员', 'aksrt');
|
||||
$vip_description = $desc_top !== '' ? $desc_top : __('开通会员,尊享专属特权', 'aksrt');
|
||||
$vip_expiry_text = __('到期时间:未开通', 'aksrt');
|
||||
// 未开通时不留到期文案:状态标签已显示「未开通」,参考主题对从未开通
|
||||
// 用户同样不输出任何到期信息(仅生效中/永久/已过期才有)。
|
||||
$vip_expiry_text = '';
|
||||
$vip_is_permanent = false;
|
||||
$btn_type = 'pay';
|
||||
|
||||
if ($level > 0) {
|
||||
$vip_level_number = $level;
|
||||
$vip_level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$vip_level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$level_desc = (string) ($levels[$level]['desc'] ?? '');
|
||||
$vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt');
|
||||
|
||||
@@ -59,7 +63,7 @@ if ($level > 0) {
|
||||
$vip_state_label = __('生效中', 'aksrt');
|
||||
$vip_expiry_text = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires));
|
||||
if ($base_level > 0 && $base_level < $level) {
|
||||
$base_name = (string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level));
|
||||
$base_name = wp_strip_all_tags((string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level)));
|
||||
$base_expiry_text = $base_expires > 0
|
||||
? wp_date('Y年m月d日', $base_expires)
|
||||
: __('永久', 'aksrt');
|
||||
@@ -71,7 +75,7 @@ if ($level > 0) {
|
||||
$vip_state = 'expired';
|
||||
$vip_state_label = __('已过期', 'aksrt');
|
||||
$vip_level_number = $expired_level;
|
||||
$vip_level_name = (string) ($levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$vip_level_name = (string) ($levels[$expired_level]['name_html'] ?? $levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$expired_desc = (string) ($levels[$expired_level]['desc'] ?? '');
|
||||
$vip_description = $expired_desc !== ''
|
||||
? $expired_desc
|
||||
@@ -91,11 +95,12 @@ $status_html = '<div class="uc-vip-status" role="status">'
|
||||
. '<span class="uc-vip-status__state">' . esc_html($vip_state_label) . '</span>'
|
||||
. '</div>'
|
||||
. '<div class="uc-vip-status__main uc-vip-status__main--level-' . (int) $vip_level_number . '">'
|
||||
. '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>'
|
||||
. '<span class="uc-vip-status__level">' . esc_html($vip_level_name) . '</span>'
|
||||
// 未开通(level 0)不输出图标位:名称前不放通用证书图标。
|
||||
. ($vip_level_number > 0 ? '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>' : '')
|
||||
. '<span class="uc-vip-status__level">' . wp_kses_post($vip_level_name) . '</span>'
|
||||
. '</div>'
|
||||
. '<div class="uc-vip-status__desc">' . esc_html($vip_description) . '</div>'
|
||||
. '<div class="uc-vip-status__expiry"><i class="fa fas fa-clock" aria-hidden="true"></i><span>' . esc_html($vip_expiry_text) . '</span></div>'
|
||||
. '<div class="uc-vip-status__desc">' . wp_kses_post($vip_description) . '</div>'
|
||||
. ($vip_expiry_text !== '' ? '<div class="uc-vip-status__expiry"><i class="fa fas fa-clock" aria-hidden="true"></i><span>' . esc_html($vip_expiry_text) . '</span></div>' : '')
|
||||
. '</div>';
|
||||
|
||||
// Level cards.
|
||||
@@ -104,15 +109,18 @@ foreach ([1, 2] as $lv) {
|
||||
if (empty($levels[$lv]['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$lv_name = (string) ($levels[$lv]['name_html'] ?? $levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$lv_desc = (string) ($levels[$lv]['desc'] ?? '');
|
||||
// Preserve admin equity HTML as-is (sanitize/wrap only; never re-split into items).
|
||||
$equity_raw = (string) ($levels[$lv]['equity'] ?? '');
|
||||
$equity = function_exists('aksrt_format_vip_equity_html')
|
||||
? aksrt_format_vip_equity_html($equity_raw)
|
||||
: '';
|
||||
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
||||
$cta = __('开通', 'aksrt') . wp_kses_post($lv_name);
|
||||
$vip_attr = $lv;
|
||||
// 参考主题 zibpay_get_vip_card:续费/升级档位卡展示各自的一句话简介。
|
||||
$card_action = 'pay';
|
||||
$card_desc = $lv_desc;
|
||||
$lv_icon_url = esc_url((string) ($levels[$lv]['img_icon'] ?? ''));
|
||||
$lv_icon = $lv_icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||
@@ -120,9 +128,11 @@ foreach ([1, 2] as $lv) {
|
||||
if (str_starts_with($btn_type, 'renew_')) {
|
||||
$renew_lv = (int) substr($btn_type, 6);
|
||||
if ($lv === $renew_lv) {
|
||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||
$cta = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'renew';
|
||||
} elseif ($lv > $renew_lv) {
|
||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'upgrade';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -130,25 +140,32 @@ foreach ([1, 2] as $lv) {
|
||||
if ($lv <= $level) {
|
||||
continue;
|
||||
}
|
||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'upgrade';
|
||||
$vip_attr = 2;
|
||||
} elseif ($level > 0 && $lv < $level) {
|
||||
continue;
|
||||
} elseif ($level > 0 && $lv === $level) {
|
||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||
$cta = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'renew';
|
||||
}
|
||||
if ($card_action === 'renew' && $renew_desc !== '') {
|
||||
$card_desc = $renew_desc;
|
||||
} elseif ($card_action === 'upgrade' && $upgrade_desc !== '') {
|
||||
$card_desc = $upgrade_desc;
|
||||
}
|
||||
|
||||
$cards .= '<div class="aksrt-panel text-center pay-vip-info flex1 level-' . $lv . ' uc-vip-level-card">';
|
||||
$cards .= '<div class="relative uc-vip-level-card__inner">';
|
||||
$cards .= '<div class="v-iconbox mb10">';
|
||||
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . esc_html($lv_name) . '</div>';
|
||||
if ($lv_desc !== '') {
|
||||
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($lv_desc) . '</div>';
|
||||
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . wp_kses_post($lv_name) . '</div>';
|
||||
if ($card_desc !== '') {
|
||||
$cards .= '<div class="v-desc em09 muted-2-color">' . wp_kses_post($card_desc) . '</div>';
|
||||
}
|
||||
$cards .= '</div>';
|
||||
$cards .= $equity;
|
||||
$cards .= '<a class="padding-lg but jb-vip' . $lv . ' radius pay-vip uc-vip-cta" href="javascript:;" vip-level="' . (int) $vip_attr . '">'
|
||||
. '<span>' . esc_html($cta) . '</span>'
|
||||
. '<span>' . $cta . '</span>'
|
||||
. ' <i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></a>';
|
||||
$cards .= '</div></div>';
|
||||
}
|
||||
@@ -169,10 +186,4 @@ foreach ([1, 2] as $lv) {
|
||||
// 下方的全宽横条;仅非会员 + 积分兑换开启 + 有可兑换套餐时输出)。
|
||||
echo aksrt_get_vip_points_exchange_entry_html($user_id, $level); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
|
||||
?>
|
||||
|
||||
<?php if (!empty($vip_settings['more'])) : ?>
|
||||
<div class="muted-2-color em09 muted-box padding-10 mini-radius mt15">
|
||||
<?php echo $vip_settings['more']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
+14
-12
@@ -363,18 +363,20 @@ function aksrt_captcha_sanitize_provider_config(string $provider, $raw): array {
|
||||
*/
|
||||
function aksrt_captcha_scenes(): array {
|
||||
return [
|
||||
'comment' => __('发表评论', 'aksrt'),
|
||||
'login' => __('用户登录', 'aksrt'),
|
||||
'register' => __('用户注册', 'aksrt'),
|
||||
'bind' => __('绑定第三方', 'aksrt'),
|
||||
'bind_phone' => __('绑定手机', 'aksrt'),
|
||||
'lostpassword' => __('找回密码', 'aksrt'),
|
||||
'change_password' => __('修改密码', 'aksrt'),
|
||||
'change_email' => __('更改邮箱', 'aksrt'),
|
||||
'download' => __('资源下载', 'aksrt'),
|
||||
'signin' => __('每日签到', 'aksrt'),
|
||||
'front_submit' => __('前台投稿', 'aksrt'),
|
||||
'tip_user' => __('打赏用户', 'aksrt'),
|
||||
'comment' => __('发表评论', 'aksrt'),
|
||||
'login' => __('用户登录', 'aksrt'),
|
||||
'register' => __('用户注册', 'aksrt'),
|
||||
'bind' => __('绑定第三方', 'aksrt'),
|
||||
'bind_phone' => __('绑定手机', 'aksrt'),
|
||||
'lostpassword' => __('找回密码', 'aksrt'),
|
||||
'change_password' => __('修改密码', 'aksrt'),
|
||||
'change_email' => __('更改邮箱', 'aksrt'),
|
||||
'download' => __('资源下载', 'aksrt'),
|
||||
'signin' => __('每日签到', 'aksrt'),
|
||||
'front_submit' => __('前台投稿', '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 {
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_status'],
|
||||
'permission_callback' => [$this, 'check_logged_in'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/settings', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'seo_generate'],
|
||||
'permission_callback' => [$this, 'check_logged_in'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'seo_suggest'],
|
||||
'permission_callback' => [$this, 'check_write_assist'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'summary_suggest'],
|
||||
@@ -76,66 +58,6 @@ class AIController extends ControllerBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get_status(): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
||||
return $this->success(['data' => [
|
||||
'enabled' => false,
|
||||
'provider' => 'none',
|
||||
'configured' => false,
|
||||
'available' => false,
|
||||
'backend' => 'none',
|
||||
]]);
|
||||
}
|
||||
|
||||
return $this->success(['data' => \AKSRT\Ai\AI::get_instance()->get_status()]);
|
||||
}
|
||||
|
||||
public function update_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$allowed = [
|
||||
'enabled' => 'bool',
|
||||
'provider' => 'string',
|
||||
'api_key' => 'string',
|
||||
'api_base' => 'url',
|
||||
'model' => 'string',
|
||||
'max_tokens' => 'int',
|
||||
'rate_limit' => 'int',
|
||||
];
|
||||
|
||||
$config = [];
|
||||
foreach ($allowed as $key => $type) {
|
||||
if (!isset($params[$key])) {
|
||||
continue;
|
||||
}
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
$config[$key] = !empty($params[$key]);
|
||||
break;
|
||||
case 'int':
|
||||
$config[$key] = (int) $params[$key];
|
||||
break;
|
||||
case 'url':
|
||||
$config[$key] = esc_url_raw((string) $params[$key]);
|
||||
break;
|
||||
default:
|
||||
$config[$key] = sanitize_text_field((string) $params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$success = \AKSRT\Ai\AI::get_instance()->update_config($config);
|
||||
if ($success) {
|
||||
return $this->success([
|
||||
'message' => __('AI 设置已保存', 'aksrt'),
|
||||
'data' => \AKSRT\Ai\AI::get_instance()->get_status(),
|
||||
]);
|
||||
}
|
||||
return $this->error(__('保存失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
||||
*/
|
||||
@@ -170,32 +92,6 @@ class AIController extends ControllerBase {
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
public function seo_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AISEO')) {
|
||||
return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
$post_id = absint($params['post_id'] ?? 0);
|
||||
|
||||
if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
|
||||
return $this->error(__('权限不足', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
if ($title === '') {
|
||||
return $this->error(__('标题不能为空', 'aksrt'));
|
||||
}
|
||||
|
||||
$result = \AKSRT\Ai\AISEO::get_instance()->suggest($title, $content);
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
||||
}
|
||||
|
||||
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||
@@ -224,7 +120,7 @@ class AIController extends ControllerBase {
|
||||
$ai = \AKSRT\Ai\AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return $this->error(
|
||||
__('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'),
|
||||
__('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||
503
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
|
||||
|
||||
/** 每页条数默认值;列表页可由 ice_perpage 覆盖(无界面控件,仅供 URL / 程序化调用)。 */
|
||||
public const DEFAULT_PER_PAGE = 20;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var string 当前页面 URL */
|
||||
@@ -119,7 +116,8 @@ class PayChargeCards {
|
||||
$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';
|
||||
$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。
|
||||
if ($status === '0' || $status === 'active') {
|
||||
@@ -297,7 +295,7 @@ class PayChargeCards {
|
||||
echo '</form>';
|
||||
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') {
|
||||
$args['desc'] = 'ASC';
|
||||
}
|
||||
if ($per_page !== self::DEFAULT_PER_PAGE) {
|
||||
if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) {
|
||||
$args['ice_perpage'] = (string) $per_page;
|
||||
}
|
||||
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 {
|
||||
?>
|
||||
<style>
|
||||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||||
.order-header .subsubsub { margin: 6px 0; }
|
||||
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
||||
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
||||
.tablenav-pages a.disabled { pointer-events: none; }
|
||||
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
||||
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
||||
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
||||
|
||||
@@ -171,7 +171,7 @@ class PayCoupons {
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||
$type_labels = [
|
||||
'fixed' => __('固定金额', 'aksrt'),
|
||||
'percent' => __('折扣百分比', 'aksrt'),
|
||||
@@ -199,7 +199,21 @@ class PayCoupons {
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
]);
|
||||
$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 '<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>';
|
||||
}
|
||||
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||
$query_args = array_filter([
|
||||
'status' => $status !== '' ? $status : null,
|
||||
's' => $search !== '' ? $search : null,
|
||||
], static function ($value): bool {
|
||||
return $value !== null;
|
||||
});
|
||||
|
||||
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 {
|
||||
@@ -457,20 +479,4 @@ class PayCoupons {
|
||||
private function page_url(array $args = []): string {
|
||||
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_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
|
||||
? \AKSRT\Payment\Order::get_valid_statuses()
|
||||
: [];
|
||||
@@ -124,7 +124,31 @@ class PayIncome {
|
||||
$result = $svc->get_list($list_args);
|
||||
$items = $result['items'] ?? [];
|
||||
$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 '<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 '</tbody></table>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
@@ -256,19 +280,4 @@ class PayIncome {
|
||||
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'));
|
||||
},
|
||||
],
|
||||
'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' => [
|
||||
'slug' => 'aksrt_pay_products',
|
||||
'title' => __('付费文章', 'aksrt'),
|
||||
|
||||
+429
-136
@@ -11,6 +11,7 @@ class PayOrders {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_orders';
|
||||
public const PAGE_AFTER_SALES_SLUG = 'aksrt_after_sales';
|
||||
|
||||
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_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_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 {
|
||||
@@ -42,20 +44,6 @@ class PayOrders {
|
||||
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'])) : '';
|
||||
if ($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_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||
$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)) {
|
||||
$product_type = '';
|
||||
@@ -130,20 +118,38 @@ class PayOrders {
|
||||
? \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.
|
||||
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
|
||||
$tab_bits = [];
|
||||
foreach ($type_tabs as $tkey => $tlabel) {
|
||||
$url = $this->page_url('aksrt_orders', array_filter([
|
||||
'product_type' => $tkey !== '' ? $tkey : 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,
|
||||
]));
|
||||
$url = $this->page_url('aksrt_orders', array_filter(
|
||||
['product_type' => $tkey !== '' ? $tkey : null] + $query_args,
|
||||
static function ($value): bool {
|
||||
return $value !== null;
|
||||
}
|
||||
));
|
||||
$cls = ($product_type === $tkey) ? 'current' : '';
|
||||
$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;">';
|
||||
$m_bits = [];
|
||||
foreach ($method_tabs as $mkey => $mlabel) {
|
||||
$url = $this->page_url('aksrt_orders', 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' => $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,
|
||||
]));
|
||||
$url = $this->page_url('aksrt_orders', array_filter(
|
||||
['payment_method' => $mkey !== '' ? $mkey : null] + $query_args,
|
||||
static function ($value): bool {
|
||||
return $value !== null;
|
||||
}
|
||||
));
|
||||
$cls = ($pay_method === $mkey) ? 'current' : '';
|
||||
$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 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>';
|
||||
// 已支付是已退款的唯一前置状态;退款仍需落到一条进行中的售后工单上。
|
||||
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);
|
||||
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')) . '\');">';
|
||||
@@ -282,19 +293,36 @@ class PayOrders {
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($pages > 1) {
|
||||
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>';
|
||||
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 售后服务子菜单(aksrt_after_sales):原订单明细页「售后」Tab 独立成页。
|
||||
*/
|
||||
public function render_after_sales_admin_page(): void {
|
||||
$this->ensure_cap();
|
||||
$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();
|
||||
}
|
||||
|
||||
@@ -323,6 +351,13 @@ class PayOrders {
|
||||
|
||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||
|
||||
// 售后记录区块:工单卡片 + 处理记录列表(与售后面板共用 .aksrt-as-log 样式)。
|
||||
echo '<style>'
|
||||
. '.aksrt-as-ticket{padding:10px 0;border-bottom:1px solid #f0f0f1;}'
|
||||
. '.aksrt-as-ticket:last-child{padding-bottom:0;border-bottom:0;}'
|
||||
. '.aksrt-as-ticket__head{margin:0 0 4px;}'
|
||||
. $this->after_sale_log_css()
|
||||
. '</style>';
|
||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||
$rows = [
|
||||
__('订单号', 'aksrt') => $order_number,
|
||||
@@ -343,12 +378,17 @@ class PayOrders {
|
||||
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
||||
if ($after_sales) {
|
||||
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
||||
$status_labels = ['pending' => '待审核', 'processing' => '处理中', 'approved' => '已通过', 'rejected' => '已拒绝', 'cancelled' => '已取消', 'completed' => '已完成'];
|
||||
$status_labels = $this->after_sale_status_labels();
|
||||
foreach ($after_sales as $application) {
|
||||
echo '<p><strong>' . esc_html($status_labels[(string) ($application['status'] ?? '')] ?? (string) ($application['status'] ?? '')) . '</strong> · ' . esc_html((string) ($application['refund_amount'] ?? '')) . '<br>' . esc_html((string) ($application['reason'] ?? ''));
|
||||
if (!empty($application['admin_note'])) echo '<br>' . esc_html__('处理备注:', 'aksrt') . esc_html((string) $application['admin_note']);
|
||||
if (!empty($application['gateway_result'])) echo '<br><code>' . esc_html((string) $application['gateway_result']) . '</code>';
|
||||
echo '</p>';
|
||||
$as_status = (string) ($application['status'] ?? '');
|
||||
echo '<div class="aksrt-as-ticket is-' . esc_attr($as_status) . '">';
|
||||
echo '<p class="aksrt-as-ticket__head"><strong>' . esc_html($status_labels[$as_status] ?? $as_status) . '</strong>'
|
||||
. ' <span class="description">' . esc_html((string) ($application['refund_amount'] ?? '')) . '</span>'
|
||||
. ' <span class="description">' . esc_html((string) ($application['created_at'] ?? '')) . '</span></p>';
|
||||
echo '<p>' . nl2br(esc_html((string) ($application['reason'] ?? ''))) . '</p>';
|
||||
// 处理记录:后台每一次提交都留一条(状态 + 备注 + 时间),与售后面板同款标记。
|
||||
echo $this->render_after_sale_log((array) ($application['handling_log'] ?? []), $status_labels); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
@@ -372,90 +412,320 @@ class PayOrders {
|
||||
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 {
|
||||
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
||||
$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_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_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];
|
||||
$items = $service->admin_list($filter_args + ['limit' => 100]);
|
||||
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" /><input type="hidden" name="tab" value="after-sales" />';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$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>';
|
||||
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> ';
|
||||
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>';
|
||||
if (!$items) echo '<tr><td colspan="7">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
||||
echo '</form>';
|
||||
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) {
|
||||
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||
$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_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 => '已完成'];
|
||||
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
||||
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>';
|
||||
if ((string) $item['status'] === 'pending') foreach (['approve' => '通过', 'reject' => '拒绝'] as $decision => $label) {
|
||||
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>';
|
||||
}
|
||||
if ((string) $item['status'] === 'processing') {
|
||||
$gateway_result = json_decode((string) ($item['gateway_result'] ?? ''), true);
|
||||
$manual_refund = is_array($gateway_result) && sanitize_key((string) ($gateway_result['status'] ?? '')) === 'manual';
|
||||
foreach (($manual_refund ? ['complete' => '确认线下退款完成', 'reject' => '确认未退款并拒绝'] : ['approve' => '同步退款状态']) as $decision => $label) {
|
||||
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>';
|
||||
$product_name = $order ? (string) (\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '';
|
||||
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
||||
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
|
||||
$apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null;
|
||||
if ($apply_form) {
|
||||
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
||||
$material_lines = [];
|
||||
foreach ((array) ($apply_form['materials'] ?? []) as $m) {
|
||||
$ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? ''));
|
||||
if ($ml === '' && $mv === '') continue;
|
||||
$material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv);
|
||||
}
|
||||
$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>';
|
||||
if (!empty($item['gateway_result'])) {
|
||||
$result_data = json_decode((string) $item['gateway_result'], true);
|
||||
$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'];
|
||||
echo '<small>' . esc_html__('渠道结果:', 'aksrt') . ' ' . esc_html($result_text) . '</small>';
|
||||
$image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string'));
|
||||
$thumbs = [];
|
||||
foreach ($image_urls as $url) {
|
||||
// 不写原生 loading 懒加载属性:lazyload 契约测试全仓库禁 loading=lazy/eager,
|
||||
// 且后台不加载 lazySizes 运行时,写了也不会被主题那套接管。
|
||||
// 与 UsersMenusVerify 的后台缩略图保持一致(光 <img src>)。
|
||||
$thumbs[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener"><img src="' . esc_url($url) . '" alt="" class="aksrt-as-thumb" /></a>';
|
||||
}
|
||||
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>';
|
||||
// 翻页链接显式带上当前筛选条件(不含 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'));
|
||||
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
||||
check_admin_referer('aksrt_after_sale_review_' . $id);
|
||||
$decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
|
||||
check_admin_referer('aksrt_after_sale_status_' . $id);
|
||||
$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'])) : '';
|
||||
$result = \AKSRT\Payment\AfterSaleService::get_instance()->review($id, $decision, $note);
|
||||
$args = ['tab' => 'after-sales', 'aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_review_failed' : 'after_sale_reviewed'];
|
||||
$mark_refund = isset($_POST['mark_order_refunded']) && (string) wp_unslash($_POST['mark_order_refunded']) === '1';
|
||||
|
||||
// 勾选「标记订单已退款」时先落订单状态:售后工单一旦离开 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);
|
||||
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 {
|
||||
@@ -482,6 +752,47 @@ class PayOrders {
|
||||
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 {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
@@ -570,8 +881,6 @@ class PayOrders {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
@@ -622,6 +931,11 @@ class PayOrders {
|
||||
$map = [
|
||||
'order_updated' => __('订单状态已更新。', '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_failed' => __('订单备注保存失败。', 'aksrt'),
|
||||
'orders_cleared' => sprintf(
|
||||
@@ -638,15 +952,17 @@ class PayOrders {
|
||||
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
|
||||
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
|
||||
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
|
||||
'after_sale_reviewed' => __('售后审核已处理。', 'aksrt'),
|
||||
'after_sale_review_failed' => __('售后审核失败,请查看申请状态和错误原因。', 'aksrt'),
|
||||
'after_sale_updated' => __('售后状态已更新。', 'aksrt'),
|
||||
'after_sale_updated_order_refunded' => __('售后状态已更新,关联订单已标记为已退款。', 'aksrt'),
|
||||
'after_sale_order_refund_failed' => __('关联订单标记已退款失败:需售后工单仍为待审核/处理中,且订单为已支付。', 'aksrt'),
|
||||
'after_sale_update_failed' => __('售后状态更新失败,请查看申请状态和错误原因。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$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>';
|
||||
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>';
|
||||
}
|
||||
}
|
||||
@@ -654,27 +970,4 @@ class PayOrders {
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
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'])) : '';
|
||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
$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();
|
||||
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
||||
@@ -51,9 +51,22 @@ class PayProducts {
|
||||
]);
|
||||
|
||||
$posts = $query['posts'];
|
||||
$total = $query['total'];
|
||||
$total = (int) $query['total'];
|
||||
$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 = [];
|
||||
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$ids = array_map(static function ($p) {
|
||||
@@ -146,8 +159,16 @@ class PayProducts {
|
||||
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>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
@@ -352,20 +373,4 @@ class PayProducts {
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
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_to = isset($_GET['date_to']) ? $this->sanitize_date((string) wp_unslash($_GET['date_to'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
$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));
|
||||
|
||||
// 页码越界(手工改 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 '<thead><tr>';
|
||||
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||
@@ -184,18 +211,7 @@ class PayWithdrawals {
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($pages > 1) {
|
||||
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>';
|
||||
}
|
||||
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
@@ -278,19 +294,4 @@ class PayWithdrawals {
|
||||
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_by = $this->current_orderby();
|
||||
$o_icon = '<i class="opacity5 ml3 fa fa-long-arrow-' . ($order === 'asc' ? 'down' : 'up') . '"></i>';
|
||||
$o_icon = $order === 'asc'
|
||||
? '<i class="opacity5 ml3 fa fas fa-long-arrow-alt-down"></i>'
|
||||
: '<i class="opacity5 ml3 fa fas fa-long-arrow-alt-up"></i>';
|
||||
|
||||
$new_columns = [];
|
||||
foreach ($columns as $key => $label) {
|
||||
|
||||
@@ -68,7 +68,7 @@ class GlobalSchema {
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
||||
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需先在 WordPress 配置 AI Connectors。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
||||
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
||||
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
||||
@@ -79,12 +79,6 @@ class GlobalSchema {
|
||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||
]],
|
||||
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
|
||||
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
||||
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
||||
|
||||
@@ -25,16 +25,11 @@ class SchemaExtensionsPush {
|
||||
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'channel', 'title' => '收录通道', 'fields' => [
|
||||
['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
||||
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
||||
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
|
||||
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
|
||||
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
||||
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
||||
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
||||
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
||||
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class SchemaPayment {
|
||||
['key' => 'points_pass_exchange_desc', 'label' => '卡密兑换积分说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
['key' => 'points_pass_exchange_only_password', 'label' => '积分卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '积分卡密仅需输入密码,不需要卡号'],
|
||||
['key' => 'pay_vip_pass_charge_s', 'label' => '启用卡密兑换会员功能', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员'],
|
||||
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
['key' => 'pay_vip_pass_charge_only_password', 'label' => '会员卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)'],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
||||
@@ -178,7 +178,7 @@ class SchemaPayment {
|
||||
'vip' => [
|
||||
'key' => 'vip',
|
||||
'title' => 'VIP会员',
|
||||
'version' => '1.1.0',
|
||||
'version' => '1.2.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||
@@ -188,17 +188,8 @@ class SchemaPayment {
|
||||
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
||||
['key' => 'nav_user_pay_vip', 'label' => '用户框购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在导航栏用户框内显示开通会员按钮。'],
|
||||
['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_1_name', 'label' => '一级会员显示名称', 'type' => 'input', 'default' => '一级会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享一级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '留空使用内置 vip-1.svg 图标'],
|
||||
['key' => 'vip_level_order_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'danger', 'dependsOn' => ['all' => [['key' => 'pay_user_vip_2_s', 'value' => true], ['key' => 'pay_user_vip_1_s', 'operator' => 'falsy']]], 'content' => '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>'],
|
||||
['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_2_name', 'label' => '二级会员显示名称', 'type' => 'input', 'default' => '二级会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享二级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '留空使用内置 vip-2.svg 图标'],
|
||||
['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'benefit', 'title' => '会员权益', 'fields' => [
|
||||
['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [
|
||||
@@ -206,11 +197,11 @@ class SchemaPayment {
|
||||
['key' => 'pay_download_limit', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '普通用户每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
||||
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => [
|
||||
['key' => 'pay_download_limit_vip_1', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '一级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed_vip_1', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
||||
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => [
|
||||
['key' => 'pay_download_limit_vip_2', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '二级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed_vip_2', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
@@ -218,36 +209,110 @@ class SchemaPayment {
|
||||
]],
|
||||
['key' => 'opt', 'title' => '会员购买选项', 'fields' => [
|
||||
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
||||
['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'payment_vip_1_products', 'label' => '一级会员套餐', 'type' => 'textarea', 'default' => "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_1_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
||||
['key' => 'vip_2_product_s', 'label' => '允许购买二级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'payment_vip_2_products', 'label' => '二级会员套餐', 'type' => 'textarea', 'default' => "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_2_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => self::vip_level_tab_fields(1)],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => self::vip_level_tab_fields(2)],
|
||||
['key' => 'renew', 'title' => '会员续费', 'fields' => [
|
||||
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'],
|
||||
['key' => 'vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_renew', 'value' => true]],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['key' => 'vip_renew_desc', 'label' => '续费介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '立即续费会员,畅享VIP权益', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'help' => '续费会员的一句话简介,支持HTML'],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'select', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['label' => '相同价格', 'value' => 'same'],
|
||||
['label' => '折扣价格', 'value' => 'discount'],
|
||||
['label' => '优惠立减', 'value' => 'reduce'],
|
||||
]],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
||||
['label' => '自定义商品', 'value' => 'customize'],
|
||||
], 'help' => '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价'],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣', 'type' => 'slider', 'sanitize_type' => 'float', 'default' => 8, 'min' => 0.1, 'max' => 9.9, 'step' => 0.1, 'unit' => '折', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount'], 'help' => '在会员商品基础上打几折,如 8 = 八折'],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'spinner', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 500, 'step' => 5, 'unit' => '元', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce'], 'help' => '在会员商品基础上优惠的金额,优惠后总金额不能为0元'],
|
||||
[
|
||||
'key' => 'vip_1_renew_product', 'label' => '一级会员续费', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加续费商品', 'default' => [
|
||||
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||
'help' => '一级会员续费的商品选项',
|
||||
...self::vip_product_group_options(),
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
[
|
||||
'key' => 'vip_2_renew_product', 'label' => '二级会员续费', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加续费商品', 'default' => [
|
||||
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||
'help' => '二级会员续费的商品选项',
|
||||
...self::vip_product_group_options(),
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
]],
|
||||
['key' => 'upgrade', 'title' => '会员升级', 'fields' => [
|
||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]],
|
||||
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题'],
|
||||
['key' => 'vip_upgrade_desc', 'label' => '升级介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '升级VIP会员,享更多会员权益', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '升级会员的一句话简介,支持HTML'],
|
||||
[
|
||||
'key' => 'vip_upgrade_product', 'label' => '升级价格', 'type' => 'accordion', 'default' => [
|
||||
'month_to_month_s' => true, 'unit_price' => '10.00', 'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'unit_tag_class' => 'jb-yellow',
|
||||
'jump_s' => false, 'jump_price' => '199.00', 'jump_show_price' => '299.00', 'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'jump_tag_class' => 'jb-red',
|
||||
'permanent_price' => '199.00', 'permanent_show_price' => '299.00', 'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'permanent_tag_class' => 'jb-red',
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_upgrade', 'value' => true],
|
||||
'help' => '会员升级的商品选项',
|
||||
'sections' => [
|
||||
['title' => '月费会员升级月费会员', 'keys' => ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class']],
|
||||
['title' => '月费会员升级永久会员', 'keys' => ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class']],
|
||||
['title' => '永久会员升级永久会员', 'keys' => ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class']],
|
||||
],
|
||||
'fields' => [
|
||||
['key' => '_upgrade_unit_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价'],
|
||||
['key' => 'month_to_month_s', 'label' => '月费会员升级月费会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'unit_price', 'label' => '天单价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true]],
|
||||
['key' => 'unit_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'rows' => 1, 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'unit_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['all' => [['key' => 'month_to_month_s', 'value' => true], ['key' => 'unit_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||
['key' => '_upgrade_jump_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项'],
|
||||
['key' => 'jump_s', 'label' => '月费会员升级永久会员', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'jump_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '月费会员升级永久会员的价格'],
|
||||
['key' => 'jump_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'jump_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'jump_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['all' => [['key' => 'jump_s', 'value' => true], ['key' => 'jump_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||
['key' => 'permanent_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'help' => '永久会员升级永久会员的价格'],
|
||||
['key' => 'permanent_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'permanent_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'permanent_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['key' => 'permanent_tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||
],
|
||||
],
|
||||
]],
|
||||
]],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '积分兑换会员', 'fields' => [
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;普通用户可开通,已有会员可续费同等级或升级更高等级)'],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]],
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费'],
|
||||
[
|
||||
'key' => 'pay_vip_points_exchange_product', 'label' => '积分兑换明细', 'type' => 'group', 'max' => 16,
|
||||
'button_title' => '添加积分兑换商品', 'default' => [
|
||||
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true],
|
||||
'help' => '每一行为一个可兑换的会员套餐',
|
||||
'collapsible' => true,
|
||||
'defaultCollapsed' => true,
|
||||
'listPreview' => ['prefix' => '积分:', 'titleKey' => 'points', 'descKey' => 'time', 'descUnitKey' => 'unit'],
|
||||
'fields' => [
|
||||
['key' => 'points', 'label' => '使用积分', 'type' => 'number', 'default' => 100, 'min' => 0, 'unit' => '积分'],
|
||||
['key' => 'level', 'label' => '兑换会员', 'type' => 'radio', 'default' => 1, 'inline' => true, 'options' => [
|
||||
['label' => '一级会员', 'value' => 1],
|
||||
['label' => '二级会员', 'value' => 2],
|
||||
]],
|
||||
['key' => 'time', 'label' => '兑换时长', 'type' => 'input', 'default' => '', 'help' => '填 Permanent 为永久'],
|
||||
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'day', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'neq', 'value' => 'Permanent'], 'options' => [
|
||||
['label' => '天', 'value' => 'day'],
|
||||
['label' => '月', 'value' => 'month'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '用户使用积分兑换时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
]],
|
||||
['key' => 'modal-copy', 'title' => '界面文案', 'fields' => [
|
||||
['key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]], 'help' => '显示在开通界面顶部的一句话简介,支持HTML'],
|
||||
['key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>', 'rows' => 4, 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
@@ -536,6 +601,19 @@ class SchemaPayment {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function vip_product_group_options(): array {
|
||||
return [
|
||||
'collapsible' => true,
|
||||
'defaultCollapsed' => true,
|
||||
'listPreview' => ['prefix' => '价格:', 'titleKey' => 'price', 'descKey' => 'time', 'descUnitKey' => 'unit'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal / Stripe 结算货币选项(参考主题同款 26 种)。
|
||||
*
|
||||
@@ -576,4 +654,92 @@ class SchemaPayment {
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品/续费商品的统一字段集(参考主题 CFS_Module::vip_product 1:1)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_fields(): array {
|
||||
return [
|
||||
['key' => 'price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '699.00', 'min' => 0, 'suffix' => '元', 'help' => '会员的实际购买价格'],
|
||||
['key' => 'show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '999.00', 'min' => 0, 'suffix' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['key' => 'tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||
['key' => 'time', 'label' => '会员有效时间', 'type' => 'spinner', 'default' => 3, 'min' => 0, 'max' => 3600, 'step' => 1, 'help' => '开通会员的时长。填0则为永久会员'],
|
||||
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'month', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'truthy'], 'options' => [
|
||||
['label' => '天', 'value' => 'day'],
|
||||
['label' => '月', 'value' => 'month'],
|
||||
]],
|
||||
['key' => '_time_permanent_hint', 'label' => '', 'type' => 'alert', 'alertType' => 'success', 'ui_only' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'eq', 'value' => 0], 'content' => '会员有效时间已设置为:永久会员'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级 Tab 的字段集(参考主题 CFS_Module::vip_tab 1:1,显示名称并入 Tab)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_level_tab_fields(int $level): array {
|
||||
$dep = ['key' => 'pay_user_vip_' . $level . '_s', 'value' => true];
|
||||
$equity = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||
return [
|
||||
['key' => 'pay_user_vip_' . $level . '_name', 'label' => '会员显示名称', 'type' => 'input', 'sanitize_type' => 'html', 'default' => $level === 1 ? '黄金会员' : '钻石会员', 'dependsOn' => $dep, 'help' => '会员名称,支持HTML'],
|
||||
['key' => 'pay_user_vip_' . $level . '_desc', 'label' => '一句话简介', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '尊享专属特权', 'dependsOn' => $dep, 'help' => '支持HTML'],
|
||||
['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => $equity, 'rows' => 4, 'dependsOn' => $dep, 'help' => '支持安全HTML,建议使用 <li> 列表项'],
|
||||
['key' => 'vip_' . $level . '_product_s', 'label' => '购买会员', 'type' => 'switch', 'default' => true, 'dependsOn' => $dep, 'help' => '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能'],
|
||||
[
|
||||
'key' => 'vip_' . $level . '_product', 'label' => '会员商品', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加会员商品', 'default' => self::vip_product_defaults(),
|
||||
...self::vip_product_group_options(),
|
||||
'dependsOn' => ['all' => [$dep, ['key' => 'vip_' . $level . '_product_s', 'value' => true]]],
|
||||
'help' => '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
['key' => 'vip_' . $level . '_img_icon', 'label' => '会员图标', 'type' => 'upload', 'default' => '', 'dependsOn' => $dep, 'help' => '留空使用内置 vip-' . $level . '.svg 图标;建议尺寸300x300'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_defaults(): array {
|
||||
return [
|
||||
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签颜色调色板(参考主题 zib_palette(array(), array('b','jb')))。
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
private static function vip_tag_palette(): array {
|
||||
$colors = [
|
||||
'b-red' => '#f74b3d',
|
||||
'b-yellow' => '#f3920a',
|
||||
'b-cyan' => '#08c4c1',
|
||||
'b-blue' => '#0a8cf3',
|
||||
'b-green' => '#1fd05a',
|
||||
'b-purple' => '#c133f5',
|
||||
'b-black' => '#121517',
|
||||
'jb-red' => 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)',
|
||||
'jb-pink' => 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)',
|
||||
'jb-yellow' => 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)',
|
||||
'jb-cyan' => 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)',
|
||||
'jb-blue' => 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)',
|
||||
'jb-green' => 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)',
|
||||
'jb-purple' => 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)',
|
||||
'jb-vip1' => 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)',
|
||||
'jb-vip2' => 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)',
|
||||
];
|
||||
$options = [];
|
||||
foreach ($colors as $value => $css) {
|
||||
$options[] = ['label' => $css, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,8 @@ class SchemaPermissions {
|
||||
['label' => '每日签到', 'value' => 'signin'],
|
||||
['label' => '前台投稿', 'value' => 'front_submit'],
|
||||
['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],
|
||||
|
||||
@@ -611,36 +611,6 @@ class SchemaUsers {
|
||||
]],
|
||||
],
|
||||
],
|
||||
'wechat' => [
|
||||
'key' => 'wechat',
|
||||
'title' => '微信通知',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'account', 'title' => '公众号配置', 'fields' => [
|
||||
['key' => 'wechat_notify_enabled', 'label' => '启用微信通知', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'wechat_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'verify', 'title' => '服务器验证', 'fields' => [
|
||||
['key' => 'wechat_token', 'label' => 'Token', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'templates', 'title' => '模板消息', 'fields' => [
|
||||
['key' => 'wechat_template_comment', 'label' => '评论通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_template_reply', 'label' => '回复通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_template_system', 'label' => '系统通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_template_order', 'label' => '订单通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'triggers', 'title' => '通知触发器', 'fields' => [
|
||||
['key' => 'wechat_notify_comment', 'label' => '新评论通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_notify_reply', 'label' => '评论回复通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_notify_system', 'label' => '系统通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_notify_order', 'label' => '订单通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_bind_entry', 'label' => '显示绑定入口', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -866,6 +866,47 @@ class SchemaProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a submitted enum value to the value declared by its option.
|
||||
*
|
||||
* Payloads reach the sanitizer as strings (or numbers), so a strict match
|
||||
* against the schema would reject a valid choice whenever the option value
|
||||
* is not itself a string -- e.g. 积分兑换明细.level declares 1 / 2 while the
|
||||
* SPA submits them as numbers and sanitize_text_field() then turns them into
|
||||
* "1" / "2". Returning the declared value keeps the stored type aligned with
|
||||
* the schema, which the radio/select controls compare with === when they
|
||||
* re-render a saved setting.
|
||||
*
|
||||
* Returns null when nothing matches, so validation still reports the
|
||||
* invalid value instead of silently rewriting the operator's input.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array<int,mixed> $options
|
||||
* @return mixed
|
||||
*/
|
||||
private function match_option_value($value, array $options) {
|
||||
foreach ($options as $opt) {
|
||||
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
|
||||
if ($opt_value === $value) {
|
||||
return $opt_value;
|
||||
}
|
||||
}
|
||||
if (is_bool($value) || !is_scalar($value)) {
|
||||
return null;
|
||||
}
|
||||
$needle = (string) $value;
|
||||
foreach ($options as $opt) {
|
||||
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
|
||||
if (is_bool($opt_value) || !is_scalar($opt_value)) {
|
||||
continue;
|
||||
}
|
||||
if ((string) $opt_value === $needle) {
|
||||
return $opt_value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function sanitize_field_value($value, array $field) {
|
||||
if (!empty($field['captcha_provider'])) {
|
||||
return function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
@@ -898,6 +939,15 @@ class SchemaProvider {
|
||||
return $normalized !== null ? $normalized : (string) ($field['default'] ?? '');
|
||||
}
|
||||
|
||||
// Enum fields keep the option's declared value so the stored type always
|
||||
// matches what validate() compares against with in_array(..., true).
|
||||
if (in_array($type, ['select', 'radio', 'palette', 'image_select'], true) && empty($field['multiple']) && !empty($field['options'])) {
|
||||
$matched = $this->match_option_value($value, (array) $field['options']);
|
||||
if ($matched !== null) {
|
||||
return $matched;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($type, ['fieldset', 'accordion'], true)) {
|
||||
return $this->sanitize_object_fields($value, (array) ($field['fields'] ?? []));
|
||||
}
|
||||
@@ -994,6 +1044,12 @@ class SchemaProvider {
|
||||
if (!is_array($field) || empty($field['key'])) {
|
||||
continue;
|
||||
}
|
||||
// UI-only chrome (notice / alert rows) is presentation only and must
|
||||
// never become a persisted settings key, matching the top-level
|
||||
// behaviour of collect_persisted_fields().
|
||||
if (!empty($field['ui_only']) || ($field['type'] ?? '') === 'notice') {
|
||||
continue;
|
||||
}
|
||||
$key = (string) $field['key'];
|
||||
if (array_key_exists($key, $value)) {
|
||||
$clean[$key] = $this->sanitize_field_value($value[$key], $field);
|
||||
|
||||
@@ -325,6 +325,54 @@ class UserController extends ControllerBase {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /wechat/status — 微信模板消息配置状态。
|
||||
*
|
||||
* 状态形状与 WeChatNotify::get_status() 对齐(该方法的既定用途即 REST 与 AJAX 复用)。
|
||||
*/
|
||||
public function get_wechat_status(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||||
return $this->error(__('微信通知组件未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
return $this->success(['data' => \AKSRT\Payment\WeChatNotify::get_instance()->get_status()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /wechat/settings — 更新微信模板消息配置。
|
||||
*
|
||||
* 前端在密钥留空且已配置时发送掩码 ***,此时保留已存密钥;
|
||||
* 未提供的字段沿用当前值。键白名单由 Integration::update_wechat_settings() 收敛。
|
||||
*/
|
||||
public function update_wechat_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$current = \AKSRT\Settings\Settings::get_wechat_settings();
|
||||
|
||||
$enabled = array_key_exists('enabled', $params) ? !empty($params['enabled']) : !empty($current['enabled']);
|
||||
$app_id = array_key_exists('app_id', $params)
|
||||
? sanitize_text_field((string) $params['app_id'])
|
||||
: (string) ($current['app_id'] ?? '');
|
||||
$template_id = array_key_exists('template_id', $params)
|
||||
? sanitize_text_field((string) $params['template_id'])
|
||||
: (string) ($current['template_id'] ?? '');
|
||||
|
||||
// 掩码 *** 表示「不修改密钥」;仅在显式传入其他值时才覆盖。
|
||||
if (array_key_exists('app_secret', $params) && (string) $params['app_secret'] !== '***') {
|
||||
$app_secret = sanitize_text_field((string) $params['app_secret']);
|
||||
} else {
|
||||
$app_secret = (string) ($current['app_secret'] ?? '');
|
||||
}
|
||||
|
||||
\AKSRT\Settings\Settings::update_wechat_settings([
|
||||
'enabled' => $enabled,
|
||||
'app_id' => $app_id,
|
||||
'app_secret' => $app_secret,
|
||||
'template_id' => $template_id,
|
||||
]);
|
||||
|
||||
return $this->success(['message' => __('微信通知配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
||||
|
||||
+9
-314
@@ -11,9 +11,6 @@ class AI {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const PROVIDER_NONE = 'none';
|
||||
const PROVIDER_OPENAI = 'openai';
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
@@ -21,95 +18,10 @@ class AI {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 配置(主题侧 provider)
|
||||
*/
|
||||
public function get_config(): array {
|
||||
$defaults = [
|
||||
'enabled' => false,
|
||||
'provider' => self::PROVIDER_NONE,
|
||||
'api_key' => '',
|
||||
'api_base' => 'https://api.openai.com/v1',
|
||||
'model' => 'gpt-3.5-turbo',
|
||||
'max_tokens' => 1000,
|
||||
'rate_limit' => 10,
|
||||
];
|
||||
|
||||
$saved = get_option('aksrt_ai_settings', []);
|
||||
if (!is_array($saved)) {
|
||||
$saved = [];
|
||||
}
|
||||
|
||||
return array_merge($defaults, $saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 AI 配置
|
||||
*/
|
||||
public function update_config(array $config): bool {
|
||||
$current = $this->get_config();
|
||||
$merged = array_merge($current, $config);
|
||||
|
||||
// Preserve existing key when client sends the masked placeholder.
|
||||
if (isset($config['api_key']) && $config['api_key'] === '***') {
|
||||
$merged['api_key'] = $current['api_key'];
|
||||
}
|
||||
|
||||
$merged['enabled'] = !empty($merged['enabled']);
|
||||
$merged['provider'] = sanitize_key((string) ($merged['provider'] ?? self::PROVIDER_NONE));
|
||||
$merged['api_key'] = (string) ($merged['api_key'] ?? '');
|
||||
$merged['api_base'] = $this->normalize_api_base((string) ($merged['api_base'] ?? 'https://api.openai.com/v1'));
|
||||
$merged['model'] = sanitize_text_field((string) ($merged['model'] ?? 'gpt-3.5-turbo'));
|
||||
$merged['max_tokens'] = max(100, min(4000, (int) ($merged['max_tokens'] ?? 1000)));
|
||||
$merged['rate_limit'] = max(1, min(100, (int) ($merged['rate_limit'] ?? 10)));
|
||||
|
||||
// Persist only the known keys so shape stays stable.
|
||||
$to_store = [
|
||||
'enabled' => $merged['enabled'],
|
||||
'provider' => $merged['provider'],
|
||||
'api_key' => $merged['api_key'],
|
||||
'api_base' => $merged['api_base'],
|
||||
'model' => $merged['model'],
|
||||
'max_tokens' => $merged['max_tokens'],
|
||||
'rate_limit' => $merged['rate_limit'],
|
||||
];
|
||||
|
||||
$before = get_option('aksrt_ai_settings', []);
|
||||
$updated = update_option('aksrt_ai_settings', $to_store);
|
||||
if ($updated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// update_option returns false when the value is unchanged — treat as success.
|
||||
if (is_array($before)) {
|
||||
$before_normalized = array_merge([
|
||||
'enabled' => false,
|
||||
'provider' => self::PROVIDER_NONE,
|
||||
'api_key' => '',
|
||||
'api_base' => 'https://api.openai.com/v1',
|
||||
'model' => 'gpt-3.5-turbo',
|
||||
'max_tokens' => 1000,
|
||||
'rate_limit' => 10,
|
||||
], $before);
|
||||
$before_normalized['enabled'] = !empty($before_normalized['enabled']);
|
||||
$before_normalized['provider'] = sanitize_key((string) ($before_normalized['provider'] ?? self::PROVIDER_NONE));
|
||||
$before_normalized['api_key'] = (string) ($before_normalized['api_key'] ?? '');
|
||||
$before_normalized['api_base'] = esc_url_raw((string) ($before_normalized['api_base'] ?? 'https://api.openai.com/v1'));
|
||||
$before_normalized['model'] = sanitize_text_field((string) ($before_normalized['model'] ?? 'gpt-3.5-turbo'));
|
||||
$before_normalized['max_tokens'] = max(100, min(4000, (int) ($before_normalized['max_tokens'] ?? 1000)));
|
||||
$before_normalized['rate_limit'] = max(1, min(100, (int) ($before_normalized['rate_limit'] ?? 10)));
|
||||
|
||||
return maybe_serialize($before_normalized) === maybe_serialize($to_store)
|
||||
|| $before_normalized == $to_store;
|
||||
}
|
||||
|
||||
return maybe_serialize($before) === maybe_serialize($to_store);
|
||||
}
|
||||
|
||||
/**
|
||||
* WordPress 核心 AI Client 是否真正可用。
|
||||
*
|
||||
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应抢占主题 provider。
|
||||
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应声称可用。
|
||||
*/
|
||||
public function is_wp_ai_available(): bool {
|
||||
if (!function_exists('wp_ai_client_prompt')) {
|
||||
@@ -124,89 +36,24 @@ class AI {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题 OpenAI 兼容 provider 是否可用。
|
||||
*/
|
||||
public function is_theme_provider_available(): bool {
|
||||
$config = $this->get_config();
|
||||
$provider = sanitize_key((string) ($config['provider'] ?? self::PROVIDER_NONE));
|
||||
$api_key = trim((string) ($config['api_key'] ?? ''));
|
||||
$api_base = trim((string) ($config['api_base'] ?? ''));
|
||||
|
||||
return !empty($config['enabled'])
|
||||
&& $provider !== self::PROVIDER_NONE
|
||||
&& $provider !== ''
|
||||
&& $api_key !== ''
|
||||
&& $api_base !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 任一后端可用即可生成。
|
||||
* AI 生成是否可用(连接由 WordPress AI Connectors 提供)。
|
||||
*/
|
||||
public function is_available(): bool {
|
||||
return $this->is_theme_provider_available() || $this->is_wp_ai_available();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前优先后端标识。
|
||||
*
|
||||
* Theme provider wins when explicitly configured — WP AI stubs often exist
|
||||
* without a working connector and previously produced opaque 400 failures.
|
||||
*/
|
||||
public function resolve_backend(): string {
|
||||
if ($this->is_theme_provider_available()) {
|
||||
return 'theme';
|
||||
}
|
||||
if ($this->is_wp_ai_available()) {
|
||||
return 'wp_ai';
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 状态(不回显完整密钥)。
|
||||
*/
|
||||
public function get_status(): array {
|
||||
$config = $this->get_config();
|
||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||
|
||||
return [
|
||||
'enabled' => !empty($config['enabled']),
|
||||
'provider' => (string) ($config['provider'] ?? self::PROVIDER_NONE),
|
||||
'configured' => !empty($config['api_key']) || $this->is_wp_ai_available(),
|
||||
'model' => (string) ($config['model'] ?? ''),
|
||||
'api_base' => (string) ($config['api_base'] ?? ''),
|
||||
'api_key' => !empty($config['api_key']) ? '***' : '',
|
||||
'max_tokens' => (int) ($config['max_tokens'] ?? 1000),
|
||||
'rate_limit' => (int) ($config['rate_limit'] ?? 10),
|
||||
'available' => $this->is_available(),
|
||||
'backend' => $this->resolve_backend(),
|
||||
'wp_ai_available' => $this->is_wp_ai_available(),
|
||||
'theme_available' => $this->is_theme_provider_available(),
|
||||
'seo_enabled' => !empty($seo['seo_enabled']),
|
||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']),
|
||||
'write_assist' => $this->is_available(),
|
||||
];
|
||||
return $this->is_wp_ai_available();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一文本生成出口。
|
||||
*
|
||||
* @param string $prompt 用户提示 / 上下文
|
||||
* @param array $options system_instruction, max_tokens, temperature
|
||||
* @param array $options system_instruction(其余选项由 WP AI Client 忽略)
|
||||
* @return array{success:bool,content?:string,message?:string,backend?:string}
|
||||
*/
|
||||
public function generate_text(string $prompt, array $options = []): array {
|
||||
if (!$this->is_available()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->check_rate_limit()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('请求过于频繁,请稍后再试', 'aksrt'),
|
||||
'message' => __('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -214,42 +61,12 @@ class AI {
|
||||
? (string) $options['system_instruction']
|
||||
: __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt');
|
||||
|
||||
$last_error = [
|
||||
'success' => false,
|
||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
||||
];
|
||||
|
||||
// 1) Prefer explicitly configured theme OpenAI-compatible provider.
|
||||
if ($this->is_theme_provider_available()) {
|
||||
$theme_result = $this->generate_via_theme($prompt, $system, $options);
|
||||
$this->record_call();
|
||||
if (!empty($theme_result['success'])) {
|
||||
$theme_result['backend'] = 'theme';
|
||||
return $theme_result;
|
||||
}
|
||||
$last_error = $theme_result;
|
||||
// Fall through to WP AI if available.
|
||||
$result = $this->generate_via_wp_ai($prompt, $system, $options);
|
||||
if (!empty($result['success'])) {
|
||||
$result['backend'] = 'wp_ai';
|
||||
}
|
||||
|
||||
// 2) WordPress AI Client (Connectors).
|
||||
if ($this->is_wp_ai_available()) {
|
||||
$wp_result = $this->generate_via_wp_ai($prompt, $system, $options);
|
||||
$this->record_call();
|
||||
if (!empty($wp_result['success'])) {
|
||||
$wp_result['backend'] = 'wp_ai';
|
||||
return $wp_result;
|
||||
}
|
||||
$last_error = $wp_result;
|
||||
}
|
||||
|
||||
return $last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧调用。
|
||||
*/
|
||||
public function generate(string $prompt, array $options = []): array {
|
||||
return $this->generate_text($prompt, $options);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function generate_via_wp_ai(string $prompt, string $system, array $options): array {
|
||||
@@ -289,126 +106,4 @@ class AI {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize OpenAI-compatible base URL.
|
||||
*
|
||||
* Accepts gateway root (https://host), /v1, trailing slash, or full
|
||||
* .../chat/completions and returns a base that can append /chat/completions.
|
||||
*/
|
||||
public function normalize_api_base(string $api_base): string {
|
||||
$api_base = trim($api_base);
|
||||
if ($api_base === '') {
|
||||
return 'https://api.openai.com/v1';
|
||||
}
|
||||
|
||||
$api_base = esc_url_raw($api_base);
|
||||
$api_base = rtrim($api_base, '/');
|
||||
|
||||
// If user pasted the full chat endpoint, strip the leaf.
|
||||
if (preg_match('#/chat/completions$#i', $api_base)) {
|
||||
$api_base = (string) preg_replace('#/chat/completions$#i', '', $api_base);
|
||||
$api_base = rtrim($api_base, '/');
|
||||
}
|
||||
|
||||
// Common gateways (NewAPI etc.): root host needs /v1.
|
||||
if (!preg_match('#/(v1|openai/v1)$#i', $api_base)) {
|
||||
$api_base .= '/v1';
|
||||
}
|
||||
|
||||
return rtrim($api_base, '/');
|
||||
}
|
||||
|
||||
private function generate_via_theme(string $prompt, string $system, array $options): array {
|
||||
$config = $this->get_config();
|
||||
$max_tokens = (int) ($options['max_tokens'] ?? $config['max_tokens']);
|
||||
$temperature = (float) ($options['temperature'] ?? 0.7);
|
||||
$api_base = $this->normalize_api_base((string) $config['api_base']);
|
||||
|
||||
$response = wp_remote_post($api_base . '/chat/completions', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $config['api_key'],
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'model' => $config['model'],
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $system],
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
],
|
||||
'max_tokens' => $max_tokens,
|
||||
'temperature' => $temperature,
|
||||
]),
|
||||
'timeout' => 45,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: error message */
|
||||
__('API 请求失败: %s', 'aksrt'),
|
||||
$response->get_error_message()
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status_code !== 200) {
|
||||
$error_msg = is_array($body) ? ($body['error']['message'] ?? __('未知错误', 'aksrt')) : __('未知错误', 'aksrt');
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: 1: HTTP status, 2: error message */
|
||||
__('API 错误 (%1$d): %2$s', 'aksrt'),
|
||||
$status_code,
|
||||
$error_msg
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$message = is_array($body) ? ($body['choices'][0]['message'] ?? []) : [];
|
||||
$content = is_array($message) ? (string) ($message['content'] ?? '') : '';
|
||||
|
||||
if ($content === '' && is_array($message) && !empty($message['reasoning_content'])) {
|
||||
$content = (string) $message['reasoning_content'];
|
||||
}
|
||||
|
||||
$content = trim($content);
|
||||
if ($content === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('AI 返回内容为空(可能 max_tokens 不足)', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'content' => $content,
|
||||
'reasoning' => is_array($message) ? (string) ($message['reasoning_content'] ?? '') : '',
|
||||
'usage' => is_array($body) ? ($body['usage'] ?? []) : [],
|
||||
'finish_reason' => is_array($body) ? (string) ($body['choices'][0]['finish_reason'] ?? '') : '',
|
||||
];
|
||||
}
|
||||
|
||||
private function check_rate_limit(): bool {
|
||||
$config = $this->get_config();
|
||||
$limit = (int) $config['rate_limit'];
|
||||
if ($limit <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
||||
$count = (int) get_transient($key);
|
||||
|
||||
return $count < $limit;
|
||||
}
|
||||
|
||||
private function record_call(): void {
|
||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
||||
$count = (int) get_transient($key);
|
||||
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
@@ -77,31 +77,6 @@ class AIPrompt {
|
||||
6. 直接输出描述文本本身,不要任何前缀、引号或解释。';
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧单 prompt 构建(草稿/无 ID 场景)。
|
||||
*/
|
||||
public function build_seo_prompt(string $title, string $content): string {
|
||||
$title_instruction = $this->get_seo_title_system_instruction();
|
||||
$kw_instruction = $this->get_seo_keywords_system_instruction();
|
||||
$desc_instruction = $this->get_seo_description_system_instruction();
|
||||
|
||||
return <<<PROMPT
|
||||
请根据以下文章内容,生成 SEO 优化建议。
|
||||
|
||||
文章标题:{$title}
|
||||
|
||||
文章内容:
|
||||
{$content}
|
||||
|
||||
请以 JSON 格式返回,包含以下字段:
|
||||
- suggested_title: {$title_instruction}
|
||||
- suggested_description: {$desc_instruction}
|
||||
- suggested_keywords: {$kw_instruction}(以 JSON 数组形式返回)
|
||||
|
||||
只返回 JSON,不要其他内容。
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
public function build_summary_prompt(string $content, int $max_length = 200): string {
|
||||
$content = mb_substr($content, 0, 3000);
|
||||
return <<<PROMPT
|
||||
|
||||
@@ -107,67 +107,6 @@ class AISEO {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿场景:用传入 title/content 生成(兼容旧 /ai/seo-suggest)。
|
||||
*/
|
||||
public function suggest(string $title, string $content = ''): array {
|
||||
if (!$this->is_enabled()) {
|
||||
return ['success' => false, 'message' => __('AI SEO 功能未启用', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT\\Ai\\AI') || !class_exists('AKSRT\\Ai\\AIPrompt')) {
|
||||
return ['success' => false, 'message' => __('AI 模块未加载', 'aksrt')];
|
||||
}
|
||||
|
||||
$ai = AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return ['success' => false, 'message' => __('AI 服务未启用', 'aksrt')];
|
||||
}
|
||||
|
||||
$title = trim($title);
|
||||
$content = trim(wp_strip_all_tags($content));
|
||||
if ($title === '' && $content === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('无可用内容,请先填写标题或正文', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($title !== '') {
|
||||
$parts[] = '标题: ' . $title;
|
||||
}
|
||||
if ($content !== '') {
|
||||
$content = wp_trim_words($content, 1500, '');
|
||||
$parts[] = "正文:\n" . $content;
|
||||
}
|
||||
$context = implode("\n\n", $parts);
|
||||
$prompt = AIPrompt::get_instance();
|
||||
|
||||
$title_out = $this->run_field($ai, $context, $prompt->get_seo_title_system_instruction(), 'title');
|
||||
$kw_out = $this->run_field($ai, $context, $prompt->get_seo_keywords_system_instruction(), 'keywords');
|
||||
$desc_out = $this->run_field($ai, $context, $prompt->get_seo_description_system_instruction(), 'description');
|
||||
|
||||
$keywords = [];
|
||||
if (!empty($kw_out['content'])) {
|
||||
$keywords = array_values(array_filter(array_map('trim', explode(',', $kw_out['content']))));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'suggested_title' => (string) ($title_out['content'] ?? ''),
|
||||
'suggested_description' => (string) ($desc_out['content'] ?? ''),
|
||||
'suggested_keywords' => $keywords,
|
||||
'fields' => [
|
||||
'title' => $title_out,
|
||||
'keywords' => $kw_out,
|
||||
'description' => $desc_out,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
|
||||
+41
-19
@@ -219,35 +219,57 @@ final class OAuth {
|
||||
'name' => $provider,
|
||||
'label' => OAuthProviderCatalog::label($provider),
|
||||
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '',
|
||||
'url' => esc_url($url),
|
||||
// Raw, matching get_frontend_bindings_data(): every surface escapes
|
||||
// once on output. esc_url() is idempotent on its own output, so
|
||||
// escaping here too was redundant rather than broken.
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference-parity social login block (legacy zib_social_login()).
|
||||
*
|
||||
* Emits the muted separator plus a .social_loginbar of .social-login-item
|
||||
* buttons, one per enabled provider. Returns '' when nothing is enabled.
|
||||
*
|
||||
* @param string $title Separator label; an empty title drops the separator.
|
||||
*/
|
||||
public function render_social_buttons(string $title = ''): string {
|
||||
$items = $this->get_auth_providers_data();
|
||||
if ($items === []) {
|
||||
return '';
|
||||
}
|
||||
ob_start();
|
||||
?>
|
||||
<div class="social-login-divider">
|
||||
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<div class="oauth-row">
|
||||
<?php foreach ($items as $item) : ?>
|
||||
<a href="<?php echo esc_url((string) $item['url']); ?>"
|
||||
class="oauth-btn oauth-btn--<?php echo esc_attr((string) $item['name']); ?>"
|
||||
data-provider="<?php echo esc_attr((string) $item['name']); ?>"
|
||||
aria-label="<?php echo esc_attr((string) $item['label']); ?>"
|
||||
title="<?php echo esc_attr((string) $item['label']); ?>">
|
||||
<?php echo $item['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
return (string) ob_get_clean();
|
||||
|
||||
$large = (bool) \AKSRT\Settings\Settings::get_value('oauth_button_lg', false);
|
||||
$html = $title !== ''
|
||||
? '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html($title) . '</p>'
|
||||
: '';
|
||||
$html .= '<div class="social_loginbar">';
|
||||
foreach ($items as $item) {
|
||||
$name = sanitize_html_class((string) ($item['name'] ?? ''));
|
||||
$type = str_starts_with($name, 'clogin_') ? substr($name, 7) : $name;
|
||||
$label = trim((string) ($item['label'] ?? ''));
|
||||
$url = (string) ($item['url'] ?? '');
|
||||
if ($name === '' || $type === '' || $label === '' || $url === '') {
|
||||
continue;
|
||||
}
|
||||
$class = 'social-login-item ' . $type . ($large ? ' button-lg' : ' toggle-radius');
|
||||
if ($name === 'weixingzh') {
|
||||
// Reference parity: the WeChat entry opens the QR panel instead of
|
||||
// navigating, so auth.js routes it to the modal's oauth tab.
|
||||
$class .= ' qrcode-signin';
|
||||
}
|
||||
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="' . esc_attr($class) . '"'
|
||||
. ' data-provider="' . esc_attr($name) . '"'
|
||||
. ' aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '"'
|
||||
. ' title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">'
|
||||
. $item['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
. ($large ? '<span>' . esc_html(sprintf(__('%s登录', 'aksrt'), $label)) . '</span>' : '')
|
||||
. '</a>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
public function get_frontend_bindings_data(int $user_id): array {
|
||||
|
||||
@@ -38,8 +38,9 @@ trait VipServiceModal {
|
||||
: wp_kses_post($equity);
|
||||
$level_view[] = [
|
||||
'level' => $level,
|
||||
'name' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
||||
'desc' => (string) ($levels[$level]['desc'] ?? ''),
|
||||
'name' => wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level))),
|
||||
'name_text' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
||||
'desc' => wp_kses_post((string) ($levels[$level]['desc'] ?? '')),
|
||||
'equity' => $equity,
|
||||
'img_icon' => esc_url_raw((string) ($levels[$level]['img_icon'] ?? '')),
|
||||
];
|
||||
@@ -115,7 +116,6 @@ trait VipServiceModal {
|
||||
'prefer_level' => $prefer_level,
|
||||
'catalog' => [
|
||||
'desc' => (string) ($catalog['desc'] ?? ''),
|
||||
'help_text' => (string) ($catalog['help_text'] ?? ''),
|
||||
'more' => (string) ($vip['more'] ?? ''),
|
||||
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
|
||||
'level_packages' => $groups,
|
||||
@@ -215,15 +215,17 @@ trait VipServiceModal {
|
||||
}
|
||||
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
|
||||
$points = max(0, (int) ($package['points'] ?? 0));
|
||||
$days = max(0, (int) ($package['days'] ?? 0));
|
||||
// 参考主题时长行:30<span class="px12">天</span> / 永久。
|
||||
$duration = $days > 0
|
||||
? (string) $days . '<span class="px12">' . esc_html__('天', 'aksrt') . '</span>'
|
||||
: esc_html__('永久', 'aksrt');
|
||||
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
// 参考主题时长行:30<span class="px12">天</span> / 永久;time 为 Permanent 时按永久渲染。
|
||||
$is_permanent = !empty($package['is_permanent']);
|
||||
$pkg_time = max(0, (int) ($package['time'] ?? 0));
|
||||
$pkg_unit = ((string) ($package['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$duration = ($is_permanent || $pkg_time <= 0)
|
||||
? esc_html__('永久', 'aksrt')
|
||||
: (string) $pkg_time . '<span class="px12">' . esc_html($pkg_unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')) . '</span>';
|
||||
$level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||
|
||||
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
||||
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . esc_html($level_name) . '</span>'
|
||||
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . wp_kses_post($level_name) . '</span>'
|
||||
. '<div class="flex jsb ac hh em16 mt10">'
|
||||
. '<span class="shrink0 mr10">' . $duration . '</span>'
|
||||
. '<span class="shrink0">' . $points . '<span class="px12">' . esc_html($currency) . '</span></span>'
|
||||
@@ -237,16 +239,18 @@ trait VipServiceModal {
|
||||
return '';
|
||||
}
|
||||
|
||||
$desc_text = trim((string) ($points_ex['desc'] ?? ''));
|
||||
$desc_text = trim((string) ($vip['points_exchange']['desc'] ?? ''));
|
||||
$desc = $desc_text !== ''
|
||||
? '<div class="muted-box muted-2-color padding-10 em09 mt20">' . esc_html($desc_text) . '</div>'
|
||||
// 管理员编辑的富文本说明:保存端已 wp_kses_post,这里同级别输出
|
||||
// (参考主题 zibpay_get_vip_points_exchange_modal 原样拼接)。
|
||||
? '<div class="muted-box muted-2-color padding-10 em09 mt20 aksrt-vip-points-desc">' . wp_kses_post($desc_text) . '</div>'
|
||||
: '';
|
||||
|
||||
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
||||
// (参考主题源码写作 `!$min_val > $user_points`,因运算符优先级恒假,
|
||||
// 此处按其原意取真实比较,保证提示分支可达。)
|
||||
if ($min_points > $user_points) {
|
||||
$form = '<div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div>';
|
||||
$form = '<div class="mt20"><div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div></div>';
|
||||
} else {
|
||||
$form = '<div class="mt20">'
|
||||
. '<input type="hidden" name="product_id" value="' . $first_package_id . '">'
|
||||
@@ -289,14 +293,14 @@ trait VipServiceModal {
|
||||
* @param array<string,mixed> $level_settings 等级设置(含 name / img_icon)。
|
||||
*/
|
||||
private function points_exchange_card_icon(array $level_settings): string {
|
||||
$name = (string) ($level_settings['name'] ?? '');
|
||||
$name_text = (string) ($level_settings['name'] ?? '');
|
||||
$icon_url = (string) ($level_settings['img_icon'] ?? '');
|
||||
if ($icon_url === '') {
|
||||
return '<i class="fa fas fa-certificate mr3" aria-hidden="true"></i>';
|
||||
}
|
||||
|
||||
return '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon mr3')
|
||||
. ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '">';
|
||||
. ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,26 +331,27 @@ trait VipServiceModal {
|
||||
if (empty($levels[$level]['enabled'])) {
|
||||
return '';
|
||||
}
|
||||
$name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$name = wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level)));
|
||||
$name_text = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$equity = (string) ($levels[$level]['equity'] ?? '');
|
||||
$cls = $user_id > 0 ? ' pay-vip' : ' signin-loader';
|
||||
|
||||
$equity_html = $equity !== ''
|
||||
? '<ul class="mb10 relative">' . wp_kses_post($equity) . '</ul>'
|
||||
: '';
|
||||
$equity_html = $equity !== '' && function_exists('aksrt_format_vip_equity_html')
|
||||
? aksrt_format_vip_equity_html($equity, 'mb10 relative')
|
||||
: ($equity !== '' ? '<div class="v-equity mb10 relative">' . wp_kses_post($equity) . '</div>' : '');
|
||||
|
||||
$icon_url = esc_url((string) ($levels[$level]['img_icon'] ?? ''));
|
||||
$icon = $icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '" decoding="async">'
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '" decoding="async">'
|
||||
: '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
||||
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . $cls . '" vip-level="' . $level . '">';
|
||||
$html .= '<div class="abs-center vip-baicon" aria-hidden="true">' . $icon . '</div>';
|
||||
$html .= '<div class="vip-img abs-right">' . $icon . '</div>';
|
||||
$html .= '<div class="relative">';
|
||||
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . esc_html($name) . '</div>';
|
||||
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . wp_kses_post($name) . '</div>';
|
||||
$html .= $equity_html;
|
||||
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
|
||||
. esc_html(sprintf(__('开通%s', 'aksrt'), $name))
|
||||
. wp_kses_post(sprintf(__('开通%s', 'aksrt'), $name))
|
||||
. '<span class="ml6"><i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></span></a>';
|
||||
$html .= '</div></div>';
|
||||
return $html;
|
||||
|
||||
+120
-314
@@ -11,82 +11,6 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parse per-level product lines (legacy vip_product).
|
||||
*
|
||||
* Preferred: price|show_price|time|unit|tag|title
|
||||
* Legacy: price|days|title
|
||||
*
|
||||
* @return array<int, array{price:string,show_price:string,time:int,unit:string,tag:string,tag_class:string,title:string,days:int}>
|
||||
*/
|
||||
public function parse_level_products(string $raw): array {
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$out = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$price_minor = $this->parse_vip_money_minor($parts[0] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$n = count($parts);
|
||||
// New format: price|show_price|time|unit|tag|title (6+)
|
||||
// Or: price|show_price|time|unit|title (5)
|
||||
if ($n >= 4 && (in_array(strtolower((string) ($parts[3] ?? '')), ['day', 'month', 'd', 'm'], true)
|
||||
|| (isset($parts[3]) && $parts[3] !== '' && !is_numeric($parts[3])))) {
|
||||
$show_minor = $this->parse_vip_money_minor($parts[1] ?? '0');
|
||||
$time = isset($parts[2]) ? (int) $parts[2] : 0;
|
||||
$unit_raw = strtolower((string) ($parts[3] ?? 'month'));
|
||||
$unit = ($unit_raw === 'day' || $unit_raw === 'd') ? 'day' : 'month';
|
||||
$tag = '';
|
||||
$title = '';
|
||||
if ($n >= 6) {
|
||||
$tag = sanitize_text_field((string) ($parts[4] ?? ''));
|
||||
$title = sanitize_text_field((string) ($parts[5] ?? ''));
|
||||
} elseif ($n === 5) {
|
||||
// price|show|time|unit|title
|
||||
$title = sanitize_text_field((string) ($parts[4] ?? ''));
|
||||
}
|
||||
} else {
|
||||
// Legacy: price|days|title
|
||||
$show_minor = 0;
|
||||
$days_legacy = isset($parts[1]) ? (int) $parts[1] : 0;
|
||||
$time = $days_legacy;
|
||||
$unit = 'day';
|
||||
$tag = '';
|
||||
$title = isset($parts[2]) ? sanitize_text_field($parts[2]) : '';
|
||||
}
|
||||
|
||||
$time = max(0, $time);
|
||||
$days = $this->duration_to_days($time, $unit);
|
||||
if ($title === '') {
|
||||
if ($time <= 0) {
|
||||
$title = __('永久会员', 'aksrt');
|
||||
} elseif ($unit === 'day') {
|
||||
$title = sprintf(__('%d天会员', 'aksrt'), $time);
|
||||
} else {
|
||||
$title = sprintf(__('%d个月会员', 'aksrt'), $time);
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor),
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'tag' => $tag,
|
||||
'tag_class' => '',
|
||||
'title' => $title,
|
||||
'days' => $days,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate days for catalog labels / order meta; activate prefers time+unit calendar math.
|
||||
*/
|
||||
@@ -100,118 +24,59 @@ trait VipServicePackages {
|
||||
return $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use parse_level_products — kept as thin adapter for upgrade fixed lines.
|
||||
*
|
||||
* @return array<int, array{price:string,days:int,title:string}>
|
||||
*/
|
||||
public function parse_packages(string $raw): array {
|
||||
$parsed = $this->parse_level_products($raw);
|
||||
$out = [];
|
||||
foreach ($parsed as $p) {
|
||||
$out[] = [
|
||||
'price' => (string) $p['price'],
|
||||
'days' => (int) $p['days'],
|
||||
'title' => (string) $p['title'],
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Build upgrade package items (legacy vip_upgrade_product parity).
|
||||
*
|
||||
* upgrade.product multi-line formats:
|
||||
* - permanent VIP1: permanent|price|title[|show_price]
|
||||
* - fixed (alias): price|days|title
|
||||
* - timed unit: unit|daily_price|title
|
||||
* - timed jump permanent: jump|price|title[|show_price]
|
||||
*
|
||||
* Rules:
|
||||
* - permanent user: only permanent/fixed (days=0 preferred)
|
||||
* - timed user: unit (keep expiry) + optional jump (to permanent)
|
||||
*
|
||||
* package index: 901 permanent/fixed, 902 unit, 903 jump
|
||||
*
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, string $raw): array {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
* Three blocks (reference theme vip_upgrade_product accordion):
|
||||
* - month_to_month_s: timed VIP1 → VIP2, priced by remaining days, expiry unchanged
|
||||
* - jump_s: timed VIP1 → permanent VIP2 at a fixed price
|
||||
* - permanent_*: permanent VIP1 → permanent VIP2 at a fixed price
|
||||
*
|
||||
* package index: 901 permanent, 902 unit, 903 jump
|
||||
*
|
||||
* @param array<string,mixed> $config
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, array $config): array {
|
||||
$items = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$has_permanent = false;
|
||||
$has_unit = false;
|
||||
$has_jump = false;
|
||||
$now = time();
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
// 月费会员升级月费会员:按剩余天数计价,会员有效期不变(仅提升等级)。
|
||||
if (!$is_permanent && $expires > $now && !empty($config['month_to_month_s'])) {
|
||||
$daily_minor = $this->parse_vip_money_minor((string) ($config['unit_price'] ?? ''));
|
||||
if ($daily_minor > 0) {
|
||||
$remaining = (int) ceil(max(1, ($expires - $now) / DAY_IN_SECONDS));
|
||||
if ($daily_minor <= intdiv(PHP_INT_MAX, $remaining)) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($remaining * $daily_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 902),
|
||||
'level' => 2,
|
||||
'index' => 902,
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => (string) ($config['unit_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['unit_tag_class'] ?? ''),
|
||||
'days' => $remaining,
|
||||
'title' => sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => '',
|
||||
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'unit',
|
||||
];
|
||||
}
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$mode = strtolower((string) ($parts[0] ?? ''));
|
||||
}
|
||||
|
||||
// unit|daily|title — timed VIP1 only.
|
||||
if ($mode === 'unit') {
|
||||
if ($is_permanent || $expires <= time() || $has_unit) {
|
||||
continue;
|
||||
}
|
||||
$daily_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($daily_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS));
|
||||
if ($daily_minor > intdiv(PHP_INT_MAX, $remaining)) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $remaining * $daily_minor;
|
||||
// 月费会员升级永久会员:固定价格直接转为永久二级会员。
|
||||
if (!$is_permanent && !empty($config['jump_s'])) {
|
||||
$price_minor = $this->parse_vip_money_minor((string) ($config['jump_price'] ?? ''));
|
||||
if ($price_minor > 0) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining);
|
||||
}
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 902),
|
||||
'level' => 2,
|
||||
'index' => 902,
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('剩余天数', 'aksrt'),
|
||||
'days' => $remaining,
|
||||
'title' => $title,
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => '',
|
||||
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'unit',
|
||||
];
|
||||
$has_unit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// jump|price|title[|show_price] — timed VIP1 only → permanent VIP2.
|
||||
if ($mode === 'jump') {
|
||||
if ($is_permanent || $has_jump) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('升级为永久会员', 'aksrt');
|
||||
}
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($config['jump_show_price'] ?? ''));
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 903),
|
||||
@@ -221,9 +86,10 @@ trait VipServicePackages {
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('永久升级', 'aksrt'),
|
||||
'discount_tag' => (string) ($config['jump_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['jump_tag_class'] ?? ''),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'title' => __('升级为永久会员', 'aksrt'),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
@@ -231,25 +97,15 @@ trait VipServicePackages {
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'jump',
|
||||
];
|
||||
$has_jump = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// permanent|price|title[|show_price]
|
||||
if ($mode === 'permanent') {
|
||||
if ($has_permanent) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
// 永久会员升级永久会员。
|
||||
if ($is_permanent) {
|
||||
$price_minor = $this->parse_vip_money_minor((string) ($config['permanent_price'] ?? ''));
|
||||
if ($price_minor > 0) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('永久会员升级', 'aksrt');
|
||||
}
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($config['permanent_show_price'] ?? ''));
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
@@ -259,9 +115,10 @@ trait VipServicePackages {
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'discount_tag' => (string) ($config['permanent_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['permanent_tag_class'] ?? ''),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'title' => __('永久会员升级', 'aksrt'),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
@@ -269,49 +126,7 @@ trait VipServicePackages {
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'permanent',
|
||||
];
|
||||
$has_permanent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// fixed: price|days|title
|
||||
$parsed = $this->parse_packages($line);
|
||||
if (empty($parsed[0]) || $has_permanent) {
|
||||
continue;
|
||||
}
|
||||
// For permanent VIP1, only accept days=0 fixed lines as permanent upgrade.
|
||||
if ($is_permanent && (int) $parsed[0]['days'] > 0) {
|
||||
continue;
|
||||
}
|
||||
$pkg = $parsed[0];
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
'level' => 2,
|
||||
'index' => 901,
|
||||
'action' => 'upgrade',
|
||||
'price' => (string) $pkg['price'],
|
||||
'base_price' => (string) $pkg['price'],
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'days' => (int) $pkg['days'],
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((string) $pkg['price']),
|
||||
'show_price_label' => '',
|
||||
'days_label' => ((int) $pkg['days'] <= 0)
|
||||
? __('永久', 'aksrt')
|
||||
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => ((int) $pkg['days'] <= 0) ? 'permanent' : 'fixed',
|
||||
];
|
||||
$has_permanent = true;
|
||||
}
|
||||
|
||||
// Permanent VIP1 should not see unit/jump.
|
||||
if ($is_permanent) {
|
||||
$items = array_values(array_filter($items, static function ($it) {
|
||||
$m = (string) ($it['upgrade_mode'] ?? '');
|
||||
return in_array($m, ['permanent', 'fixed'], true);
|
||||
}));
|
||||
}
|
||||
|
||||
return $items;
|
||||
@@ -320,7 +135,10 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
/**
|
||||
* Points exchange packages: level|points|days|title
|
||||
* Points exchange packages from the structured 积分兑换明细 group.
|
||||
*
|
||||
* Each row is points / level / time / unit, where time is the literal
|
||||
* string "Permanent" for a permanent grant (reference theme contract).
|
||||
*
|
||||
* @param array<string,mixed> $vip
|
||||
* @param array<int,mixed> $levels
|
||||
@@ -328,47 +146,49 @@ trait VipServicePackages {
|
||||
*/
|
||||
public function get_points_exchange_packages(array $vip, array $levels): array {
|
||||
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
|
||||
$raw = (string) ($points_ex['packages_raw'] ?? '');
|
||||
$rows = isset($points_ex['packages']) && is_array($points_ex['packages']) ? $points_ex['packages'] : [];
|
||||
$currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$out = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$i = 0;
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
if (count($parts) < 3) {
|
||||
continue;
|
||||
}
|
||||
$lv = (int) ($parts[0] ?? 0);
|
||||
$pts = (int) ($parts[1] ?? 0);
|
||||
$days = (int) ($parts[2] ?? 0);
|
||||
$title = sanitize_text_field((string) ($parts[3] ?? ''));
|
||||
$lv = (int) ($row['level'] ?? 0);
|
||||
$pts = max(0, (int) ($row['points'] ?? 0));
|
||||
if ($lv < 1 || $lv > 2 || $pts <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (empty($levels[$lv]['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$i++;
|
||||
if ($title === '') {
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$title = $days > 0
|
||||
? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days)
|
||||
: sprintf(__('%s · 永久', 'aksrt'), $lv_name);
|
||||
$is_permanent = !empty($row['is_permanent']);
|
||||
$time = max(0, (int) ($row['time'] ?? 0));
|
||||
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$days = $is_permanent ? 0 : max(0, (int) ($row['days'] ?? 0));
|
||||
if (!$is_permanent && $time <= 0) {
|
||||
continue;
|
||||
}
|
||||
$i++;
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$out[] = [
|
||||
'package_id' => 5000 + $i,
|
||||
'level' => $lv,
|
||||
'index' => $i,
|
||||
'points' => $pts,
|
||||
'days' => max(0, $days),
|
||||
'title' => $title,
|
||||
'price_label' => $pts . ' ' . $currency,
|
||||
'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'),
|
||||
'cta_label' => __('立即兑换', 'aksrt'),
|
||||
'package_id' => 5000 + $i,
|
||||
'level' => $lv,
|
||||
'index' => $i,
|
||||
'points' => $pts,
|
||||
'days' => $days,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'is_permanent' => $is_permanent,
|
||||
'title' => $is_permanent
|
||||
? sprintf(__('%s · 永久', 'aksrt'), $lv_name)
|
||||
: sprintf(__('%1$s · %2$d%3$s', 'aksrt'), $lv_name, $time, $unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')),
|
||||
'price_label' => $pts . ' ' . $currency,
|
||||
'days_label' => $is_permanent
|
||||
? __('永久', 'aksrt')
|
||||
: ($unit === 'month' ? sprintf(__('%d 个月', 'aksrt'), $time) : sprintf(__('%d 天', 'aksrt'), $time)),
|
||||
'duration_label' => $is_permanent ? __('永久', 'aksrt') : ($unit === 'month' ? sprintf(__('%d 个月', 'aksrt'), $time) : sprintf(__('%d 天', 'aksrt'), $time)),
|
||||
'cta_label' => __('立即兑换', 'aksrt'),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
@@ -376,41 +196,18 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Single upgrade product line: price|days|title
|
||||
*
|
||||
* @return array{price:float,days:int,title:string}|null
|
||||
*/
|
||||
public function parse_upgrade_product_line(string $raw): ?array {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '' || str_starts_with(strtolower($line), 'unit|')) {
|
||||
continue;
|
||||
}
|
||||
$parsed = $this->parse_packages($line);
|
||||
if (!empty($parsed[0])) {
|
||||
return $parsed[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Apply renew discount/reduce to same-level packages.
|
||||
*
|
||||
* `customize` reuses the dedicated renew product's own price/show_price/tag,
|
||||
* so the caller passes that row's whole item in as $pkg.
|
||||
*
|
||||
* @param array<string,mixed> $renew
|
||||
* @param array<string,mixed> $upgrade
|
||||
* @return array{price:string,show_price:string,tag:string}|null
|
||||
* @return array{price:string,show_price:string,tag:string,tag_class:string}|null
|
||||
*/
|
||||
private function apply_action_price(
|
||||
string $base_price,
|
||||
array $pkg,
|
||||
string $action,
|
||||
array $renew,
|
||||
array $upgrade,
|
||||
@@ -420,6 +217,7 @@ trait VipServicePackages {
|
||||
bool $is_permanent,
|
||||
int $days
|
||||
): ?array {
|
||||
$base_price = (string) ($pkg['price'] ?? '');
|
||||
$base_minor = $this->parse_vip_money_minor($base_price);
|
||||
if ($base_minor <= 0) {
|
||||
return null;
|
||||
@@ -434,32 +232,31 @@ trait VipServicePackages {
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($base_minor),
|
||||
'show_price' => '0.00',
|
||||
'tag' => '',
|
||||
'tag_class' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
|
||||
// discount/reduce 的标签由优惠幅度计算得出,若实际没有优惠则属于虚假折扣。
|
||||
$computed_tag = in_array($type, ['discount', 'reduce'], true);
|
||||
$price_minor = $base_minor;
|
||||
$show_minor = $base_minor;
|
||||
$tag = '';
|
||||
$tag_class = '';
|
||||
|
||||
if ($type === 'discount') {
|
||||
$discount = (int) ($renew['discount'] ?? 8);
|
||||
$discount = (float) ($renew['discount'] ?? 8);
|
||||
if ($discount <= 0) {
|
||||
$discount = 8;
|
||||
}
|
||||
if ($discount <= 10) {
|
||||
$percent = $discount * 10;
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折续费', 'aksrt'),
|
||||
(string) $discount
|
||||
);
|
||||
} else {
|
||||
$percent = min(100, $discount);
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount);
|
||||
}
|
||||
// 折值:8 = 八折(参考主题 slider 0.1–9.9 语义)。
|
||||
$discount = max(0.1, min(9.9, $discount));
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $discount * 10);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折优惠', 'aksrt'),
|
||||
rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.') ?: '0'
|
||||
);
|
||||
} elseif ($type === 'reduce') {
|
||||
$reduce_minor = $this->parse_vip_money_minor($renew['reduce'] ?? '20');
|
||||
$price_minor = max(1, $base_minor - $reduce_minor);
|
||||
@@ -468,6 +265,11 @@ trait VipServicePackages {
|
||||
__('续费立减%s', 'aksrt'),
|
||||
$this->format_cash_label(\AKSRT\Ledger\AssetAmount::decimal_from_minor($reduce_minor))
|
||||
);
|
||||
} elseif ($type === 'customize') {
|
||||
// 自定义商品:沿用商品自身的原价、促销标签与标签颜色。
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($pkg['show_price'] ?? ''));
|
||||
$tag = (string) ($pkg['tag'] ?? '');
|
||||
$tag_class = (string) ($pkg['tag_class'] ?? '');
|
||||
} else {
|
||||
$show_minor = 0;
|
||||
$tag = __('续费同价', 'aksrt');
|
||||
@@ -477,14 +279,18 @@ trait VipServicePackages {
|
||||
$price_minor = 1;
|
||||
}
|
||||
if ($show_minor > 0 && $price_minor >= $show_minor) {
|
||||
// 参考主题契约:原价仅在低于执行价时展示。
|
||||
$show_minor = 0;
|
||||
$tag = __('续费', 'aksrt');
|
||||
if ($computed_tag) {
|
||||
$tag = __('续费', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor > $price_minor ? $show_minor : 0),
|
||||
'tag' => $tag,
|
||||
'tag_class' => $tag_class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -137,22 +137,32 @@ trait VipServicePurchase {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$name = trim(wp_strip_all_tags((string) ($levels[$lv]['name'] ?? ('VIP' . $lv))));
|
||||
$items = [];
|
||||
$packages = $product_s
|
||||
? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? ''))
|
||||
: [];
|
||||
// 自定义续费商品时按续费商品组计价,否则按会员商品组计价。
|
||||
$level_action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
$level_action = 'renew';
|
||||
} elseif ($level > 0 && $lv > $level) {
|
||||
$level_action = 'upgrade';
|
||||
}
|
||||
$renew_custom = $level_action === 'renew'
|
||||
&& sanitize_key((string) ($renew['price_type'] ?? 'discount')) === 'customize';
|
||||
$packages = [];
|
||||
if ($product_s) {
|
||||
$packages = $renew_custom
|
||||
? (array) ($levels[$lv]['renew_products'] ?? [])
|
||||
: (array) ($levels[$lv]['products'] ?? []);
|
||||
}
|
||||
foreach ($packages as $idx => $pkg) {
|
||||
if (!is_array($pkg)) {
|
||||
continue;
|
||||
}
|
||||
$base_price = (string) $pkg['price'];
|
||||
$days = (int) $pkg['days'];
|
||||
$action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
$action = 'renew';
|
||||
} elseif ($level > 0 && $lv > $level) {
|
||||
$action = 'upgrade';
|
||||
}
|
||||
$action = $level_action;
|
||||
|
||||
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
|
||||
$priced = $this->apply_action_price($pkg, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
|
||||
if ($priced === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -167,6 +177,10 @@ trait VipServicePurchase {
|
||||
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
|
||||
$discount_tag = (string) $pkg['tag'];
|
||||
}
|
||||
$tag_class = (string) ($priced['tag_class'] ?? '');
|
||||
if ($tag_class === '' && !empty($pkg['tag_class'])) {
|
||||
$tag_class = (string) $pkg['tag_class'];
|
||||
}
|
||||
|
||||
$days_label = __('永久', 'aksrt');
|
||||
if ((int) $pkg['time'] > 0) {
|
||||
@@ -184,10 +198,10 @@ trait VipServicePurchase {
|
||||
'base_price' => $base_price,
|
||||
'show_price' => $show_price,
|
||||
'discount_tag' => $discount_tag,
|
||||
'tag_class' => $tag_class,
|
||||
'days' => $days,
|
||||
'time' => (int) ($pkg['time'] ?? 0),
|
||||
'unit' => (string) ($pkg['unit'] ?? 'day'),
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((string) $priced['price']),
|
||||
'show_price_label' => $show_minor > 0
|
||||
? $this->format_cash_label($show_price)
|
||||
@@ -201,7 +215,7 @@ trait VipServicePurchase {
|
||||
|
||||
// Dedicated upgrade product: fixed or remaining-days unit price.
|
||||
if ($level === 1 && $lv === 2 && $upgrade_enabled) {
|
||||
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
|
||||
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, $upgrade);
|
||||
foreach ($upgrade_items as $ui) {
|
||||
$items[] = $ui;
|
||||
}
|
||||
@@ -232,21 +246,19 @@ trait VipServicePurchase {
|
||||
}
|
||||
}
|
||||
|
||||
$points_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0;
|
||||
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
|
||||
$balances = \AKSRT\Ledger\LedgerService::get_instance()->points_and_balance($user_id);
|
||||
$methods = $this->available_payment_methods();
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
|
||||
$catalog = [
|
||||
'enabled' => !empty($vip['enabled']),
|
||||
'desc' => (string) ($vip['desc'] ?? ''),
|
||||
'help_text' => (string) ($vip['help_text'] ?? ''),
|
||||
'user_level' => $level,
|
||||
'user_expires' => $expires,
|
||||
'user_is_permanent' => $is_permanent,
|
||||
'user_expires_label' => (string) ($member['expires_label'] ?? ''),
|
||||
'user_points' => $points_balance,
|
||||
'wallet_balance' => $wallet_balance,
|
||||
'user_points' => $balances['points'],
|
||||
'wallet_balance' => $balances['balance'],
|
||||
'currency_name' => $currency_name,
|
||||
'renew_enabled' => $renew_enabled,
|
||||
'upgrade_enabled' => $upgrade_enabled,
|
||||
@@ -443,11 +455,12 @@ trait VipServicePurchase {
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => $expires_at,
|
||||
'subject' => sprintf(
|
||||
/* translators: 1: action 2: vip name 3: package title */
|
||||
__('%1$s%2$s · %3$s', 'aksrt'),
|
||||
/* translators: 1: action 2: vip name 3: price label 4: duration label */
|
||||
__('%1$s%2$s · %3$s / %4$s', 'aksrt'),
|
||||
$action_label,
|
||||
$level_name,
|
||||
(string) ($pkg['title'] ?? '')
|
||||
(string) ($pkg['price_label'] ?? ''),
|
||||
(string) ($pkg['days_label'] ?? '')
|
||||
),
|
||||
];
|
||||
|
||||
@@ -759,6 +772,11 @@ trait VipServicePurchase {
|
||||
$cost = (int) ($match['points'] ?? 0);
|
||||
$level = (int) ($match['level'] ?? 1);
|
||||
$days = (int) ($match['days'] ?? 0);
|
||||
$pkg_time = max(0, (int) ($match['time'] ?? 0));
|
||||
$pkg_unit = sanitize_key((string) ($match['unit'] ?? 'day'));
|
||||
if (!in_array($pkg_unit, ['day', 'month'], true)) {
|
||||
$pkg_unit = 'day';
|
||||
}
|
||||
if ($cost <= 0) {
|
||||
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
@@ -781,6 +799,8 @@ trait VipServicePurchase {
|
||||
'vip_pay_type' => 'points_exchange',
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'time' => $pkg_time,
|
||||
'unit' => $pkg_unit,
|
||||
'points' => $cost,
|
||||
'package_id' => $package_id,
|
||||
'package_title' => (string) ($match['title'] ?? ''),
|
||||
|
||||
+47
-44
@@ -1,44 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class DBMigrator {
|
||||
use DBMigratorPart2;
|
||||
use DBMigratorPart3;
|
||||
use DBMigratorIdentity;
|
||||
use DBMigratorVip;
|
||||
|
||||
/** Current schema version; changing it re-runs dbDelta. */
|
||||
// 2.5.1: order_aftersale 新增 images 列(售后举证图片)。dbDelta 仅在版本变化时
|
||||
// 重跑,新增/调整列必须同步升版本,否则老安装不会执行 ALTER TABLE。
|
||||
// 2.5.2: order_aftersale 新增 apply_form 列(结构化申请表单:预置原因 / 申请资料 / 申请说明)。
|
||||
const SCHEMA_VERSION = '2.5.2';
|
||||
|
||||
const OPT_KEY = 'aksrt_db_schema_version';
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action('after_switch_theme', [self::class, 'maybe_migrate']);
|
||||
add_action('admin_init', [self::class, 'maybe_migrate']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run table schema upgrade when version mismatches.
|
||||
*/
|
||||
public static function maybe_migrate(): void {
|
||||
$current = get_option(self::OPT_KEY, '');
|
||||
if ($current === self::SCHEMA_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::run();
|
||||
update_option(self::OPT_KEY, self::SCHEMA_VERSION);
|
||||
}
|
||||
}
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class DBMigrator {
|
||||
use DBMigratorPart2;
|
||||
use DBMigratorPart3;
|
||||
use DBMigratorIdentity;
|
||||
use DBMigratorVip;
|
||||
|
||||
/** Current schema version; changing it re-runs dbDelta. */
|
||||
// 2.5.1: order_aftersale 新增 images 列(售后举证图片)。dbDelta 仅在版本变化时
|
||||
// 重跑,新增/调整列必须同步升版本,否则老安装不会执行 ALTER TABLE。
|
||||
// 2.5.2: order_aftersale 新增 apply_form 列(结构化申请表单:预置原因 / 申请资料 / 申请说明)。
|
||||
// 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';
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action('after_switch_theme', [self::class, 'maybe_migrate']);
|
||||
add_action('admin_init', [self::class, 'maybe_migrate']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run table schema upgrade when version mismatches.
|
||||
*/
|
||||
public static function maybe_migrate(): void {
|
||||
$current = get_option(self::OPT_KEY, '');
|
||||
if ($current === self::SCHEMA_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::run();
|
||||
update_option(self::OPT_KEY, self::SCHEMA_VERSION);
|
||||
}
|
||||
}
|
||||
+559
-564
File diff suppressed because it is too large.
Load diff
@@ -210,16 +210,16 @@ class SEOMetaBox {
|
||||
true
|
||||
);
|
||||
|
||||
$status = class_exists('\AKSRT\Ai\AI') ? \AKSRT\Ai\AI::get_instance()->get_status() : [];
|
||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||
$ai_available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||
|
||||
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'ai' => [
|
||||
'available' => !empty($status['available']),
|
||||
'available' => $ai_available,
|
||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
||||
'write_assist' => !empty($status['available']),
|
||||
'write_assist' => $ai_available,
|
||||
],
|
||||
'i18n' => [
|
||||
'generating' => __('生成中…', 'aksrt'),
|
||||
@@ -262,7 +262,7 @@ class SEOMetaBox {
|
||||
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
||||
$title = !$available
|
||||
? esc_attr__('请先配置 AI 连接(扩展 → AI 连接配置 或 WP Connectors)', 'aksrt')
|
||||
? esc_attr__('请先配置 WordPress AI Connectors', 'aksrt')
|
||||
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
||||
|
||||
return sprintf(
|
||||
|
||||
@@ -63,7 +63,10 @@ trait ScriptsPart1b {
|
||||
};
|
||||
|
||||
// 顶部导航用户卡片(legacy header-userbox 模块样式)。
|
||||
$enqueue_module('header-userbox');
|
||||
// 仅登录用户渲染用户卡(aksrt-header-userbox.php 的渲染条件),访客页面不加载。
|
||||
if (is_user_logged_in()) {
|
||||
$enqueue_module('header-userbox');
|
||||
}
|
||||
|
||||
if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) {
|
||||
$auth_css = get_template_directory() . '/assets/css/routes/sign.css';
|
||||
|
||||
@@ -30,6 +30,9 @@ class StatsTracker {
|
||||
|
||||
/**
|
||||
* 追踪访问
|
||||
*
|
||||
* 访客判定与 setcookie 必须留在 wp_head(响应头尚未发送);
|
||||
* 计数读写改挂 shutdown,避免每次前台请求都在渲染关键路径上写库。
|
||||
*/
|
||||
public function track_visit(): void {
|
||||
// 排除管理员、爬虫、AJAX请求
|
||||
@@ -41,6 +44,12 @@ class StatsTracker {
|
||||
return;
|
||||
}
|
||||
|
||||
static $scheduled = false;
|
||||
if ($scheduled) {
|
||||
return;
|
||||
}
|
||||
$scheduled = true;
|
||||
|
||||
$today = current_time('Y-m-d');
|
||||
$option_key = 'aksrt_daily_stats_' . $today;
|
||||
|
||||
@@ -48,6 +57,19 @@ class StatsTracker {
|
||||
$visitor_cookie = 'aksrt_visitor_' . $today;
|
||||
$is_unique = empty($_COOKIE[$visitor_cookie]);
|
||||
|
||||
if ($is_unique) {
|
||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
||||
}
|
||||
|
||||
add_action('shutdown', static function () use ($option_key, $is_unique): void {
|
||||
self::get_instance()->record_stats($option_key, $is_unique);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数落库(shutdown 阶段执行;请求中断时本次统计丢弃)。
|
||||
*/
|
||||
public function record_stats(string $option_key, bool $is_unique): void {
|
||||
// 获取或初始化今日统计
|
||||
$stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]);
|
||||
|
||||
@@ -59,7 +81,6 @@ class StatsTracker {
|
||||
$stats['pageviews']++;
|
||||
if ($is_unique) {
|
||||
$stats['visitors']++;
|
||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
||||
}
|
||||
|
||||
update_option($option_key, $stats, false);
|
||||
|
||||
@@ -314,8 +314,13 @@ class Signin {
|
||||
}
|
||||
|
||||
$checkin_all_day = $total ? '<div class="mb6 em09">' . sprintf(__('累计签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $total) . '</badge>') . '</div>' : '';
|
||||
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, true);
|
||||
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
||||
// 会员图标不再挂头像角标,改到昵称行最前;禁封徽章依旧占据会员图标位。
|
||||
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, false);
|
||||
$name_badge = function_exists('aksrt_get_user_ban_name_badge') ? aksrt_get_user_ban_name_badge($user_id) : '';
|
||||
if ($name_badge === '' && function_exists('aksrt_get_vip_icon')) {
|
||||
$name_badge = aksrt_get_vip_icon($user_id);
|
||||
}
|
||||
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . $name_badge . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
||||
|
||||
// 每日签到奖励卡片(参考主题:积分徽章 + 经验值徽章,经验值随等级系统展示)
|
||||
$reward_badge = $base ? '<badge class=" p2-10 mr6 c-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('积分奖励%s', 'aksrt'), $base)) . '">' . $points_svg('icon') . '+' . esc_html((string) $base) . '</badge>' : '';
|
||||
|
||||
+279
-63
@@ -9,6 +9,14 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class SearchPush {
|
||||
|
||||
/** 推送结果 meta 键(编辑页面板与去重逻辑共用)。 */
|
||||
public const META_KEY = '_aksrt_baidu_push_back';
|
||||
|
||||
/** 「重新提交」nonce 与复选框字段名(与 SearchPushMetaBox 共用)。 */
|
||||
public const NONCE_ACTION = 'aksrt_push_meta_save';
|
||||
public const NONCE_FIELD = 'aksrt_push_meta_nonce';
|
||||
public const RESUBMIT_FIELD = 'aksrt_push_resubmit';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
@@ -17,16 +25,20 @@ class SearchPush {
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定运行时钩子:发布/更新主动推送 + 前台自动推送 JS。
|
||||
* 绑定运行时钩子:发布/更新主动推送 + 分类法保存推送 + 前台自动推送 JS。
|
||||
*/
|
||||
public function init(): void {
|
||||
add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3);
|
||||
add_action('saved_term', [$this, 'on_saved_term'], 10, 3);
|
||||
add_action('wp_footer', [$this, 'output_auto_push_script'], 99);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章发布/更新时主动推送链接到百度。
|
||||
*
|
||||
* 与参考主题一致:推送结果写入 meta,成功后不再重复提交;
|
||||
* 编辑页勾选「重新提交」可清空记录并强制再次推送。
|
||||
*
|
||||
* @param string $new_status 新状态。
|
||||
* @param string $old_status 旧状态。
|
||||
* @param \WP_Post $post 文章对象。
|
||||
@@ -42,27 +54,230 @@ class SearchPush {
|
||||
return;
|
||||
}
|
||||
// 仅推送公开可索引的内容类型。
|
||||
$allowed = function_exists('aksrt_searchable_post_types')
|
||||
? aksrt_searchable_post_types()
|
||||
: ['post'];
|
||||
if (!in_array($post->post_type, $allowed, true)) {
|
||||
if (!in_array($post->post_type, self::pushable_post_types(), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
if (!$this->should_push($config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resubmit_requested()) {
|
||||
self::clear_push_record($post->ID);
|
||||
} else {
|
||||
$is_new = $old_status !== 'publish';
|
||||
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
||||
return;
|
||||
}
|
||||
if (!$is_new && empty($config['baidu_push_on_update'])) {
|
||||
return;
|
||||
}
|
||||
// 已成功提交过的链接不再重复推送。
|
||||
if ($this->record_is_complete(self::get_push_record($post->ID), $config)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->push_and_record((string) get_permalink($post), $post->ID, false, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类法术语保存时主动推送链接到百度。
|
||||
*
|
||||
* 术语无发布/更新状态,沿用参考主题的 saved_term 触发,
|
||||
* 与文章推送共用 meta 记录、去重与「重新提交」。
|
||||
*
|
||||
* @param int $term_id 术语 ID。
|
||||
* @param int $tt_id 术语分类法 ID。
|
||||
* @param string $taxonomy 分类法名称。
|
||||
*/
|
||||
public function on_saved_term(int $term_id, int $tt_id, string $taxonomy): void {
|
||||
if (!in_array($taxonomy, self::pushable_taxonomies(), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
if (!$this->should_push($config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resubmit_requested()) {
|
||||
self::clear_push_record($term_id, true);
|
||||
} elseif ($this->record_is_complete(self::get_push_record($term_id, true), $config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = get_term_link($term_id, $taxonomy);
|
||||
if (is_wp_error($url) || $url === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->push_and_record((string) $url, $term_id, true, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可推送的文章类型(与站内搜索 / MeiliSearch 索引共用同一策略)。
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function pushable_post_types(): array {
|
||||
$types = function_exists('aksrt_searchable_post_types') ? aksrt_searchable_post_types() : ['post'];
|
||||
|
||||
return array_values(array_unique(array_filter((array) $types)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 可推送的分类法:可推送文章类型上的公开分类法(排除文章形式等)。
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function pushable_taxonomies(): array {
|
||||
$taxonomies = get_object_taxonomies(self::pushable_post_types(), 'names');
|
||||
$taxonomies = array_diff((array) $taxonomies, ['post_format', 'nav_menu']);
|
||||
|
||||
/**
|
||||
* 过滤百度推送覆盖的分类法。
|
||||
*
|
||||
* @param string[] $taxonomies 分类法 slug 列表。
|
||||
*/
|
||||
return array_values(array_unique(apply_filters('aksrt_baidu_push_taxonomies', $taxonomies)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送功能是否已启用且配置完整(决定编辑页面板是否展示)。
|
||||
*/
|
||||
public function is_ready(): bool {
|
||||
return $this->should_push($this->get_config());
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文章或术语的推送记录。
|
||||
*
|
||||
* @param int $object_id 文章 / 术语 ID。
|
||||
* @param bool $is_term 是否为术语。
|
||||
* @return array 记录为空时返回 []。
|
||||
*/
|
||||
public static function get_push_record(int $object_id, bool $is_term = false): array {
|
||||
$record = $is_term ? get_term_meta($object_id, self::META_KEY, true) : get_post_meta($object_id, self::META_KEY, true);
|
||||
|
||||
return is_array($record) ? $record : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入推送记录。
|
||||
*
|
||||
* @param int $object_id 文章 / 术语 ID。
|
||||
* @param array $record 推送记录。
|
||||
* @param bool $is_term 是否为术语。
|
||||
*/
|
||||
public static function save_push_record(int $object_id, array $record, bool $is_term = false): void {
|
||||
if ($is_term) {
|
||||
update_term_meta($object_id, self::META_KEY, $record);
|
||||
} else {
|
||||
update_post_meta($object_id, self::META_KEY, $record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除推送记录(「重新提交」时调用)。
|
||||
*
|
||||
* @param int $object_id 文章 / 术语 ID。
|
||||
* @param bool $is_term 是否为术语。
|
||||
*/
|
||||
public static function clear_push_record(int $object_id, bool $is_term = false): void {
|
||||
if ($is_term) {
|
||||
delete_term_meta($object_id, self::META_KEY);
|
||||
} else {
|
||||
delete_post_meta($object_id, self::META_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送是否可用:总开关开启、Token 已配置且至少启用一个收录通道。
|
||||
*
|
||||
* @param array $config 推送配置。
|
||||
*/
|
||||
private function should_push(array $config): bool {
|
||||
if (empty($config['baidu_push_enabled'])) {
|
||||
return false;
|
||||
}
|
||||
if ((string) ($config['baidu_push_token'] ?? '') === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !empty($config['baidu_push_normal']) || !empty($config['baidu_push_fast']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送 URL 并把结果写入 meta(成功记录用于去重)。
|
||||
*
|
||||
* @param string $url 待推送链接。
|
||||
* @param int $object_id 文章 / 术语 ID。
|
||||
* @param bool $is_term 是否为术语。
|
||||
* @param array $config 推送配置。
|
||||
*/
|
||||
private function push_and_record(string $url, int $object_id, bool $is_term, array $config): void {
|
||||
if ($url === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$is_new = $old_status !== 'publish';
|
||||
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
||||
return;
|
||||
$outcome = $this->push_url($url);
|
||||
self::save_push_record($object_id, $this->build_record($outcome, $config), $is_term);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装推送记录:通道结果 + 更新时间(结构与参考主题 meta 对齐)。
|
||||
*
|
||||
* @param array $outcome push_url() 返回值。
|
||||
* @param array $config 推送配置。
|
||||
*/
|
||||
private function build_record(array $outcome, array $config): array {
|
||||
$record = ['update_time' => current_time('Y-m-d H:i:s')];
|
||||
$result = isset($outcome['result']) && is_array($outcome['result']) ? $outcome['result'] : [];
|
||||
|
||||
if (!empty($config['baidu_push_normal'])) {
|
||||
$channel = $result['normal'] ?? null;
|
||||
$record['normal_push'] = is_array($channel) && !empty($channel['success']);
|
||||
$record['normal_result'] = $channel;
|
||||
}
|
||||
if (!$is_new && empty($config['baidu_push_on_update'])) {
|
||||
return;
|
||||
if (!empty($config['baidu_push_fast'])) {
|
||||
$channel = $result['daily'] ?? null;
|
||||
$record['daily_push'] = is_array($channel) && !empty($channel['success']);
|
||||
$record['daily_result'] = $channel;
|
||||
}
|
||||
|
||||
$this->push_url((string) get_permalink($post));
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送记录是否已覆盖全部启用通道(去重判断)。
|
||||
*
|
||||
* @param array $record 推送记录。
|
||||
* @param array $config 推送配置。
|
||||
*/
|
||||
private function record_is_complete(array $record, array $config): bool {
|
||||
if (!empty($config['baidu_push_normal']) && empty($record['normal_push'])) {
|
||||
return false;
|
||||
}
|
||||
if (!empty($config['baidu_push_fast']) && empty($record['daily_push'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑页是否勾选了「重新提交」(需通过 metabox nonce 校验)。
|
||||
*/
|
||||
private function resubmit_requested(): bool {
|
||||
if (empty($_POST[self::RESUBMIT_FIELD])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nonce = isset($_POST[self::NONCE_FIELD]) ? sanitize_text_field(wp_unslash($_POST[self::NONCE_FIELD])) : '';
|
||||
|
||||
return (bool) wp_verify_nonce($nonce, self::NONCE_ACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +311,50 @@ class SearchPush {
|
||||
return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []];
|
||||
}
|
||||
|
||||
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
|
||||
// 收录通道与百度接口 type 参数对应:普通收录不带参数,快速收录为 daily。
|
||||
$channels = [];
|
||||
if (!empty($config['baidu_push_normal'])) {
|
||||
$channels['normal'] = '';
|
||||
}
|
||||
if (!empty($config['baidu_push_fast'])) {
|
||||
$channels['daily'] = '&type=daily';
|
||||
}
|
||||
if (!$channels) {
|
||||
return ['success' => false, 'message' => __('未启用普通收录或快速收录通道', 'aksrt'), 'result' => []];
|
||||
}
|
||||
|
||||
$success = false;
|
||||
$results = [];
|
||||
$messages = [];
|
||||
|
||||
foreach ($channels as $channel => $suffix) {
|
||||
$outcome = $this->submit($url, $token, $suffix, $config);
|
||||
$results[$channel] = $outcome['result'];
|
||||
if ($outcome['success']) {
|
||||
$success = true;
|
||||
} else {
|
||||
$messages[] = $outcome['message'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $results];
|
||||
}
|
||||
|
||||
return ['success' => false, 'message' => implode('; ', $messages), 'result' => $results];
|
||||
}
|
||||
|
||||
/**
|
||||
* 向百度链接提交接口发送单条 URL。
|
||||
*
|
||||
* @param string $url 待推送的完整 URL。
|
||||
* @param string $token 百度准入密钥。
|
||||
* @param string $suffix 接口附加参数(快速收录为 &type=daily)。
|
||||
* @param array $config 推送配置。
|
||||
* @return array{success: bool, message: string, result: array}
|
||||
*/
|
||||
private function submit(string $url, string $token, string $suffix, array $config): array {
|
||||
$api_url = 'http://data.zz.baidu.com/urls?site=' . rawurlencode(home_url('/')) . '&token=' . $token . $suffix;
|
||||
|
||||
$response = wp_remote_post($api_url, [
|
||||
'timeout' => 10,
|
||||
@@ -105,7 +363,7 @@ class SearchPush {
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
$this->log('error', $url, $response->get_error_message());
|
||||
$this->log($config, 'error', $url, $response->get_error_message());
|
||||
return ['success' => false, 'message' => $response->get_error_message(), 'result' => []];
|
||||
}
|
||||
|
||||
@@ -113,59 +371,15 @@ class SearchPush {
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status === 200 && isset($body['success'])) {
|
||||
$this->log('success', $url, wp_json_encode($body));
|
||||
$this->log($config, 'success', $url, wp_json_encode($body));
|
||||
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body];
|
||||
}
|
||||
|
||||
$err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status));
|
||||
$this->log('error', $url, $err_msg);
|
||||
$this->log($config, 'error', $url, $err_msg);
|
||||
return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推送 URL
|
||||
*
|
||||
* @param string[] $urls
|
||||
* @return array{success: int, failed: int, results: array}
|
||||
*/
|
||||
public function push_batch(array $urls): array {
|
||||
$config = $this->get_config();
|
||||
$batch_size = max(1, min(500, (int) ($config['baidu_push_batch_size'] ?? 50)));
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
$results = [];
|
||||
|
||||
$chunks = array_chunk($urls, $batch_size);
|
||||
foreach ($chunks as $chunk) {
|
||||
$token = (string) ($config['baidu_push_token'] ?? '');
|
||||
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
|
||||
$body_str = implode("\n", $chunk);
|
||||
|
||||
$response = wp_remote_post($api_url, [
|
||||
'timeout' => 15,
|
||||
'headers' => ['Content-Type' => 'text/plain'],
|
||||
'body' => $body_str,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
$failed += count($chunk);
|
||||
$results[] = ['error' => $response->get_error_message()];
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
if (isset($body['success'])) {
|
||||
$success += (int) ($body['success'] ?? 0);
|
||||
$failed += (int) ($body['not_same_site'] ?? 0) + (int) ($body['not_valid'] ?? 0);
|
||||
} else {
|
||||
$failed += count($chunk);
|
||||
}
|
||||
$results[] = $body ?? [];
|
||||
}
|
||||
|
||||
return ['success' => $success, 'failed' => $failed, 'results' => $results];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自动推送 JS 代码
|
||||
*/
|
||||
@@ -175,7 +389,6 @@ class SearchPush {
|
||||
return '';
|
||||
}
|
||||
|
||||
$site_url = home_url('/');
|
||||
return <<<SCRIPT
|
||||
<script>
|
||||
(function(){
|
||||
@@ -211,13 +424,15 @@ SCRIPT;
|
||||
'provider' => 'baidu',
|
||||
'configured' => $enabled && $token !== '',
|
||||
'available' => $enabled && $token !== '',
|
||||
'verified' => !empty($config['baidu_push_verified']),
|
||||
// 百度链接提交接口无单独校验握手,Token 配置完成即可推送。
|
||||
'verified' => false,
|
||||
'transport' => 'http_api',
|
||||
'missing_fields' => $missing,
|
||||
'auto_push' => !empty($config['baidu_push_auto']),
|
||||
'normal_push' => !empty($config['baidu_push_normal']),
|
||||
'fast_push' => !empty($config['baidu_push_fast']),
|
||||
'on_publish' => !empty($config['baidu_push_on_publish']),
|
||||
'on_update' => !empty($config['baidu_push_on_update']),
|
||||
'batch_size' => max(1, (int) ($config['baidu_push_batch_size'] ?? 50)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -228,7 +443,8 @@ SCRIPT;
|
||||
return ['baidu_push_enabled' => false];
|
||||
}
|
||||
|
||||
private function log(string $level, string $url, string $message): void {
|
||||
private function log(array $config, string $level, string $url, string $message): void {
|
||||
if (empty($config['baidu_push_log_enabled'])) return;
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||
error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Extensions;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 百度推送编辑页面板(对齐参考主题「百度资源提交」Metabox)。
|
||||
*
|
||||
* 文章:侧栏 Metabox;分类法:编辑 / 新增表单行。
|
||||
* 展示推送结果与更新时间,并提供「重新提交」勾选——
|
||||
* 勾选后保存会清空去重记录并强制再次推送。
|
||||
*/
|
||||
class SearchPushMetaBox {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('add_meta_boxes', [$this, 'register_post_meta_box']);
|
||||
// 分类法可能由插件延后注册,面板在 admin_init 统一挂载。
|
||||
add_action('admin_init', [$this, 'register_term_panels']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为可推送文章类型注册侧栏 Metabox(推送功能就绪后才展示)。
|
||||
*/
|
||||
public function register_post_meta_box(): void {
|
||||
if (!$this->is_ready()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (SearchPush::pushable_post_types() as $post_type) {
|
||||
add_meta_box(
|
||||
'aksrt_baidu_push',
|
||||
__('百度资源提交', 'aksrt'),
|
||||
[$this, 'render_post_panel'],
|
||||
$post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为可推送分类法注册编辑 / 新增表单面板。
|
||||
*/
|
||||
public function register_term_panels(): void {
|
||||
if (!$this->is_ready()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (SearchPush::pushable_taxonomies() as $taxonomy) {
|
||||
add_action("{$taxonomy}_edit_form_fields", [$this, 'render_term_panel'], 20);
|
||||
add_action("{$taxonomy}_add_form_fields", [$this, 'render_term_panel'], 20);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文章 Metabox。
|
||||
*
|
||||
* @param mixed $post WP_Post 或 null。
|
||||
*/
|
||||
public function render_post_panel($post): void {
|
||||
if (!$post instanceof \WP_Post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render_panel((int) $post->ID, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染分类法表单行(新增表单无术语记录,仅展示提示)。
|
||||
*
|
||||
* @param mixed $term WP_Term、分类名字符串或 null。
|
||||
*/
|
||||
public function render_term_panel($term = null): void {
|
||||
$term_id = 0;
|
||||
if ($term instanceof \WP_Term) {
|
||||
$term_id = (int) $term->term_id;
|
||||
} elseif (isset($_GET['tag_ID'])) {
|
||||
$term_id = (int) $_GET['tag_ID'];
|
||||
}
|
||||
|
||||
// 新增表单无表格结构,仅编辑表单以表单行包裹。
|
||||
$this->render_panel($term_id, true, $term_id > 0);
|
||||
}
|
||||
|
||||
private function is_ready(): bool {
|
||||
return class_exists(SearchPush::class) && SearchPush::get_instance()->is_ready();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染推送结果面板。
|
||||
*
|
||||
* @param int $object_id 文章 / 术语 ID(0 表示新增表单)。
|
||||
* @param bool $is_term 是否为术语。
|
||||
* @param bool $as_row 是否以分类法表单行包裹。
|
||||
*/
|
||||
private function render_panel(int $object_id, bool $is_term, bool $as_row = false): void {
|
||||
ob_start();
|
||||
wp_nonce_field(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD);
|
||||
|
||||
$record = $object_id > 0 ? SearchPush::get_push_record($object_id, $is_term) : [];
|
||||
$lines = $this->result_lines($record);
|
||||
?>
|
||||
<div class="aksrt-push-meta-box">
|
||||
<?php if ($lines) : ?>
|
||||
<p class="aksrt-push-meta-box__label"><strong><?php esc_html_e('提交结果:', 'aksrt'); ?></strong></p>
|
||||
<ul class="aksrt-push-meta-box__results">
|
||||
<?php foreach ($lines as $line) : ?>
|
||||
<li><strong><?php echo esc_html($line[0]); ?></strong> <span><?php echo esc_html($line[1]); ?></span></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else : ?>
|
||||
<p class="description"><?php esc_html_e('发布、更新后刷新页面后可查看提交结果', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($record['update_time'])) : ?>
|
||||
<p class="aksrt-push-meta-box__resubmit">
|
||||
<label>
|
||||
<input type="checkbox" name="<?php echo esc_attr(SearchPush::RESUBMIT_FIELD); ?>" value="1" />
|
||||
<?php esc_html_e('重新提交', 'aksrt'); ?>
|
||||
</label>
|
||||
<span class="description"><?php esc_html_e('勾选后保存将清空提交记录并再次推送', 'aksrt'); ?></span>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
$body = (string) ob_get_clean();
|
||||
|
||||
if ($as_row) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
|
||||
echo '<tr class="form-field"><th scope="row">' . esc_html(__('百度资源提交', 'aksrt')) . '</th><td>' . $body . '</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
|
||||
echo $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总推送结果行(展示项与参考主题对齐)。
|
||||
*
|
||||
* @param array $record 推送记录。
|
||||
* @return array<int,array{0:string,1:string}>
|
||||
*/
|
||||
private function result_lines(array $record): array {
|
||||
$lines = [];
|
||||
|
||||
if (!empty($record['normal_push'])) {
|
||||
$lines[] = [__('普通收录:成功', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
|
||||
} elseif (array_key_exists('normal_push', $record)) {
|
||||
$lines[] = [__('普通收录:失败', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
|
||||
}
|
||||
|
||||
if (!empty($record['daily_push'])) {
|
||||
$lines[] = [__('快速收录:成功', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
|
||||
} elseif (array_key_exists('daily_push', $record)) {
|
||||
$lines[] = [__('快速收录:失败', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
|
||||
}
|
||||
|
||||
if (!empty($record['update_time'])) {
|
||||
$lines[] = [__('更新时间:', 'aksrt'), (string) $record['update_time']];
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化接口返回结果。
|
||||
*
|
||||
* @param mixed $result 百度接口返回的解码数据。
|
||||
*/
|
||||
private function format_result($result): string {
|
||||
if (is_array($result) || is_object($result)) {
|
||||
return (string) wp_json_encode($result);
|
||||
}
|
||||
|
||||
return (string) $result;
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,46 @@ final class LedgerService {
|
||||
return AssetAmount::from_minor(AssetType::BALANCE, $this->balance($user_id, AssetType::BALANCE))->decimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read points and cash balance together (VIP modal catalog).
|
||||
*
|
||||
* The meta adapter serves both assets from user meta; the asset-account
|
||||
* fallback answers both rows with a single query.
|
||||
*
|
||||
* @return array{points:int,balance:string}
|
||||
*/
|
||||
public function points_and_balance(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['points' => 0, 'balance' => '0.00'];
|
||||
}
|
||||
if (class_exists('\AKSRT\Credits\MetaAssetService')) {
|
||||
$meta = \AKSRT\Credits\MetaAssetService::get_instance();
|
||||
return [
|
||||
'points' => $meta->balance_points($user_id),
|
||||
'balance' => $meta->balance_decimal($user_id),
|
||||
];
|
||||
}
|
||||
global $wpdb;
|
||||
$table = $this->table('asset_account');
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT asset, balance_minor FROM {$table} WHERE user_id = %d AND asset IN (%s, %s)",
|
||||
$user_id,
|
||||
AssetType::POINTS->value,
|
||||
AssetType::BALANCE->value
|
||||
), ARRAY_A);
|
||||
$minor = ['points' => 0, 'balance' => 0];
|
||||
foreach ((array) $rows as $row) {
|
||||
$asset = (string) ($row['asset'] ?? '');
|
||||
if (isset($minor[$asset])) {
|
||||
$minor[$asset] = max(0, (int) ($row['balance_minor'] ?? 0));
|
||||
}
|
||||
}
|
||||
return [
|
||||
'points' => $minor['points'],
|
||||
'balance' => AssetAmount::from_minor(AssetType::BALANCE, $minor['balance'])->decimal(),
|
||||
];
|
||||
}
|
||||
|
||||
public function format_balance(int $minor): string {
|
||||
return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted();
|
||||
}
|
||||
|
||||
+433
-680
File diff suppressed because it is too large.
Load diff
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user