Compare commits
118
Commits
962ab5c290
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81b60eb427 | ||
|
|
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 | ||
|
|
9e3267fef4 | ||
|
|
6081e8dcc9 | ||
|
|
c801ff5047 | ||
|
|
8460132feb | ||
|
|
732f615a09 | ||
|
|
738abe5102 | ||
|
|
915311dcb6 | ||
|
|
c200427358 | ||
|
|
7b5dcf16a1 | ||
|
|
2db2414270 | ||
|
|
365b2eafe5 | ||
|
|
ea88499996 | ||
|
|
f4ff3e16d4 | ||
|
|
c67d5ae4bc | ||
|
|
00a087b1a4 | ||
|
|
a0f59826fe | ||
|
|
e7e281f10c | ||
|
|
93579cbc9f | ||
|
|
4ea02f25ac | ||
|
|
1a9a2762f3 | ||
|
|
b447c41f13 | ||
|
|
8e1fd0a30f | ||
|
|
cf6dc24193 | ||
|
|
c8fdd81d6a | ||
|
|
27868b6131 | ||
|
|
ef718042d2 | ||
|
|
194280af72 | ||
|
|
50fac36169 | ||
|
|
2595bb02db | ||
|
|
fe57022550 |
No files matched your search
@@ -5,11 +5,4 @@ export const extensionsApiMethods = {
|
|||||||
|
|
||||||
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
||||||
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
||||||
|
|
||||||
/* AI 辅助 */
|
|
||||||
getAiStatus() { return this.request('/ai/status') },
|
|
||||||
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
|
|
||||||
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
|
|
||||||
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
|
|
||||||
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
|
|
||||||
}
|
}
|
||||||
@@ -476,6 +476,34 @@
|
|||||||
|
|
||||||
<!-- Fieldset (single nested object) -->
|
<!-- Fieldset (single nested object) -->
|
||||||
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
||||||
|
<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
|
<AksField
|
||||||
v-for="field in visibleNestedFields"
|
v-for="field in visibleNestedFields"
|
||||||
:key="field.key"
|
:key="field.key"
|
||||||
@@ -484,6 +512,7 @@
|
|||||||
:form-data="formData"
|
:form-data="formData"
|
||||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
||||||
@@ -507,15 +536,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Repeater (array of objects) — inline expand (default) -->
|
<!-- Repeater (array of objects) — inline expand (default, draggable) -->
|
||||||
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
|
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
|
||||||
<div
|
<draggable
|
||||||
v-for="(item, index) in repeaterItems"
|
v-model="repeaterDragList"
|
||||||
:key="item._id || index"
|
:item-key="repeaterDragKey"
|
||||||
class="aksrt-field__repeater-item"
|
handle=".aksrt-field__repeater-handle"
|
||||||
|
ghost-class="aksrt-field__repeater-ghost"
|
||||||
|
animation="180"
|
||||||
>
|
>
|
||||||
<div class="aksrt-field__repeater-header">
|
<template #item="{ element, index }">
|
||||||
|
<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" @click.stop></i>
|
||||||
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
|
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
|
||||||
|
<span v-if="repeaterCollapsible && repeaterItemSummary(element)" class="aksrt-field__repeater-summary">
|
||||||
|
{{ repeaterItemSummary(element) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="aksrt-field__repeater-header-actions" @click.stop>
|
||||||
|
<FaIcon
|
||||||
|
v-if="repeaterCollapsible"
|
||||||
|
:icon="isRepeaterItemOpen(index) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
|
||||||
|
:size="12"
|
||||||
|
class="aksrt-field__repeater-toggle"
|
||||||
|
/>
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
type="error"
|
type="error"
|
||||||
@@ -526,12 +576,13 @@
|
|||||||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="aksrt-field__repeater-fields">
|
</div>
|
||||||
|
<div v-show="!repeaterCollapsible || isRepeaterItemOpen(index)" class="aksrt-field__repeater-fields">
|
||||||
<template v-for="field in (schema.fields || [])" :key="field.key">
|
<template v-for="field in (schema.fields || [])" :key="field.key">
|
||||||
<AksField
|
<AksField
|
||||||
v-if="matchDependency(field.dependsOn, item)"
|
v-if="matchDependency(field.dependsOn, element)"
|
||||||
:schema="field"
|
:schema="field"
|
||||||
:model-value="item[field.key]"
|
:model-value="element[field.key]"
|
||||||
:form-data="formData"
|
:form-data="formData"
|
||||||
:disabled="schema.disabled"
|
:disabled="schema.disabled"
|
||||||
@update:model-value="updateRepeaterItem(index, field.key, $event)"
|
@update:model-value="updateRepeaterItem(index, field.key, $event)"
|
||||||
@@ -539,6 +590,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
<n-button
|
<n-button
|
||||||
size="small"
|
size="small"
|
||||||
dashed
|
dashed
|
||||||
@@ -688,6 +741,8 @@ import FaIconPicker from './FaIconPicker.vue'
|
|||||||
import AksMediaField from './AksMediaField.vue'
|
import AksMediaField from './AksMediaField.vue'
|
||||||
import AksGalleryField from './AksGalleryField.vue'
|
import AksGalleryField from './AksGalleryField.vue'
|
||||||
import AksCodeEditor from './AksCodeEditor.vue'
|
import AksCodeEditor from './AksCodeEditor.vue'
|
||||||
|
import { buildFieldsetSections } from './fieldset-sections.js'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -720,6 +775,85 @@ const repeaterItems = computed(() => Array.isArray(props.modelValue) ? props.mod
|
|||||||
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
|
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
|
||||||
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
|
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* 手风琴模式:schema.collapsible 开启后,每一项默认收起,仅显示标题行,
|
||||||
|
* 点击标题行展开/收起。defaultCollapsed 为 false 时改为默认全部展开。
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
const repeaterCollapsible = computed(() => !!props.schema.collapsible)
|
||||||
|
const repeaterDefaultOpen = computed(() => props.schema.defaultCollapsed === false)
|
||||||
|
// 只记录「与默认状态相反」的下标,新增/删除项时其余项的开合状态不受影响。
|
||||||
|
const toggledRepeaterItems = ref(new Set())
|
||||||
|
|
||||||
|
function isRepeaterItemOpen(index) {
|
||||||
|
if (!repeaterCollapsible.value) return true
|
||||||
|
return repeaterDefaultOpen.value !== toggledRepeaterItems.value.has(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRepeaterItem(index) {
|
||||||
|
const next = new Set(toggledRepeaterItems.value)
|
||||||
|
if (next.has(index)) {
|
||||||
|
next.delete(index)
|
||||||
|
} else {
|
||||||
|
next.add(index)
|
||||||
|
}
|
||||||
|
toggledRepeaterItems.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收起态标题行上的一行摘要,取自 listPreview 配置。 */
|
||||||
|
function repeaterItemSummary(item) {
|
||||||
|
if (!repeaterCollapsible.value || !item) return ''
|
||||||
|
const cfg = getRepeaterPreviewConfig()
|
||||||
|
const titleKey = cfg.titleKey || 'title'
|
||||||
|
const title = String(item[titleKey] ?? '').trim()
|
||||||
|
const prefix = String(cfg.prefix || '')
|
||||||
|
if (title === '') return prefix
|
||||||
|
const desc = repeaterItemDuration(item)
|
||||||
|
return desc ? `${prefix}${title} · ${desc}` : `${prefix}${title}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** time/unit 形态的时长摘要:0 视为永久,其余按 day/month 补单位。 */
|
||||||
|
function repeaterItemDuration(item) {
|
||||||
|
const cfg = getRepeaterPreviewConfig()
|
||||||
|
const descKey = cfg.descKey
|
||||||
|
if (!descKey) return ''
|
||||||
|
const raw = item[descKey]
|
||||||
|
if (raw === '' || raw == null) return ''
|
||||||
|
const amount = Number(raw)
|
||||||
|
if (!Number.isFinite(amount)) return String(raw)
|
||||||
|
if (amount <= 0) return '永久'
|
||||||
|
const unitKey = cfg.descUnitKey
|
||||||
|
if (!unitKey) return String(raw)
|
||||||
|
const unit = String(item[unitKey] || '')
|
||||||
|
if (unit === 'month') return `${amount} 个月`
|
||||||
|
if (unit === 'day') return `${amount} 天`
|
||||||
|
return String(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拖拽视图:vuedraggable 以 modelValue 模式工作(内部复制后回传 update:modelValue)。 */
|
||||||
|
const repeaterDragList = computed({
|
||||||
|
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
|
||||||
|
set: (value) => emit('update:modelValue', Array.isArray(value) ? value : []),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行 key:优先用行自身 _id;服务端历史数据没有 _id 时按对象引用派生稳定 key,
|
||||||
|
* 避免每次输入都重算 key 导致子字段重挂载、输入焦点丢失。
|
||||||
|
*/
|
||||||
|
const dragRowKeys = new WeakMap()
|
||||||
|
let dragRowKeySeed = 0
|
||||||
|
function repeaterDragKey(item) {
|
||||||
|
if (item && typeof item === 'object') {
|
||||||
|
if (item._id) return item._id
|
||||||
|
let key = dragRowKeys.get(item)
|
||||||
|
if (!key) {
|
||||||
|
key = `rpt_drag_${++dragRowKeySeed}`
|
||||||
|
dragRowKeys.set(item, key)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return String(item)
|
||||||
|
}
|
||||||
|
|
||||||
function fieldsetValue(field) {
|
function fieldsetValue(field) {
|
||||||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||||||
? props.modelValue
|
? props.modelValue
|
||||||
@@ -727,6 +861,35 @@ function fieldsetValue(field) {
|
|||||||
return Object.prototype.hasOwnProperty.call(current, field.key) ? current[field.key] : field.default
|
return Object.prototype.hasOwnProperty.call(current, field.key) ? current[field.key] : field.default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* accordion/fieldset 的分块手风琴:schema.sections 声明每块的标题与字段
|
||||||
|
* key,默认全部收起,点击块标题展开。未声明 sections 时保持平铺渲染。
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
const fieldsetSections = computed(() => {
|
||||||
|
return buildFieldsetSections(props.schema)
|
||||||
|
})
|
||||||
|
|
||||||
|
const openFieldsetSections = ref(new Set())
|
||||||
|
|
||||||
|
function isFieldsetSectionOpen(index) {
|
||||||
|
return openFieldsetSections.value.has(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分块内字段同样遵循 dependsOn,数据源是本 fieldset 的值对象。 */
|
||||||
|
function sectionFields(section) {
|
||||||
|
return (section.fields || []).filter((field) => matchDependency(field.dependsOn, props.modelValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFieldsetSection(index) {
|
||||||
|
const next = new Set(openFieldsetSections.value)
|
||||||
|
if (next.has(index)) {
|
||||||
|
next.delete(index)
|
||||||
|
} else {
|
||||||
|
next.add(index)
|
||||||
|
}
|
||||||
|
openFieldsetSections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
function updateFieldsetValue(key, value) {
|
function updateFieldsetValue(key, value) {
|
||||||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||||||
? props.modelValue
|
? props.modelValue
|
||||||
@@ -1018,6 +1181,8 @@ const repeaterDraft = ref({})
|
|||||||
function createRepeaterItem() {
|
function createRepeaterItem() {
|
||||||
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
||||||
for (const field of (props.schema.fields || [])) {
|
for (const field of (props.schema.fields || [])) {
|
||||||
|
// UI-only chrome rows (notice / alert) are presentation only.
|
||||||
|
if (field.ui_only) continue
|
||||||
if (field.default !== undefined) {
|
if (field.default !== undefined) {
|
||||||
newItem[field.key] = field.default
|
newItem[field.key] = field.default
|
||||||
} else if (field.type === 'switch') {
|
} else if (field.type === 'switch') {
|
||||||
@@ -1047,7 +1212,16 @@ function removeRepeaterItem(index) {
|
|||||||
|
|
||||||
function updateRepeaterItem(index, fieldKey, value) {
|
function updateRepeaterItem(index, fieldKey, value) {
|
||||||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||||||
current[index] = { ...current[index], [fieldKey]: value }
|
const row = current[index]
|
||||||
|
if (row && typeof row === 'object') {
|
||||||
|
// 必须原地写,不能展开成新对象:drag key 存在以「对象身份」为键的 WeakMap
|
||||||
|
// 里,而行数据是 reactive 的——换一个新对象后,渲染时读到的是它的一个新
|
||||||
|
// reactive 代理,WeakMap 认不出它便生成新 key,整行重挂载、输入框失焦,
|
||||||
|
// 表现为打一个字就退出编辑。经代理原地写既保持身份,也照样触发响应式。
|
||||||
|
row[fieldKey] = value
|
||||||
|
} else {
|
||||||
|
current[index] = { [fieldKey]: value }
|
||||||
|
}
|
||||||
emit('update:modelValue', current)
|
emit('update:modelValue', current)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1149,6 +1323,15 @@ function confirmRepeaterModal() {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
|
||||||
|
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
|
||||||
|
.aksrt-field--group,
|
||||||
|
.aksrt-field--repeater,
|
||||||
|
.aksrt-field--accordion,
|
||||||
|
.aksrt-field--fieldset {
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
.aksrt-field__input-wrap {
|
.aksrt-field__input-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1414,6 +1597,48 @@ function confirmRepeaterModal() {
|
|||||||
background: var(--aksrt-color-canvas);
|
background: var(--aksrt-color-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aksrt-field__accordion-section + .aksrt-field__accordion-section {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-field__accordion-section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--aksrt-color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--aksrt-color-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--aksrt-color-text);
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-field__accordion-section-head:hover {
|
||||||
|
border-color: var(--aksrt-color-primary);
|
||||||
|
color: var(--aksrt-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-field__accordion-section-title {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-field__accordion-section-toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--aksrt-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-field__accordion-section-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.aksrt-field__repeater {
|
.aksrt-field__repeater {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1431,9 +1656,68 @@ function confirmRepeaterModal() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aksrt-field__repeater-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-field__repeater-ghost {
|
||||||
|
opacity: 0.45;
|
||||||
|
border-color: var(--aksrt-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.aksrt-field__repeater-index {
|
.aksrt-field__repeater-index {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -64,9 +64,9 @@
|
|||||||
<n-tabs type="line" animated>
|
<n-tabs type="line" animated>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-for="tab in field.tabs"
|
v-for="tab in field.tabs"
|
||||||
:key="tab.title || tab.key"
|
:key="tab.key || tab.title"
|
||||||
:name="tab.title || tab.key"
|
:name="tabName(tab)"
|
||||||
:tab="tab.title || tab.key"
|
:tab="tabName(tab)"
|
||||||
>
|
>
|
||||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||||
<AksField
|
<AksField
|
||||||
@@ -231,6 +231,18 @@ function isFieldVisible(field) {
|
|||||||
return matchDependsOn(field.dependsOn)
|
return matchDependsOn(field.dependsOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
|
||||||
|
* display name) so renaming a level renames its tab; `titleFallback` is used
|
||||||
|
* while that value is still empty.
|
||||||
|
*/
|
||||||
|
function tabName(tab) {
|
||||||
|
if (!tab) return ''
|
||||||
|
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
|
||||||
|
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
|
||||||
|
return label || tab.titleFallback || tab.title || tab.key || ''
|
||||||
|
}
|
||||||
|
|
||||||
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||||
function isGroupVisible(group) {
|
function isGroupVisible(group) {
|
||||||
return matchDependsOn(group.dependsOn)
|
return matchDependsOn(group.dependsOn)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 把 accordion / fieldset 的 `schema.sections` 声明展开成「块标题 + 字段定义」。
|
||||||
|
*
|
||||||
|
* 参考主题把「升级价格」这类复合配置拆成若干可折叠块,每块用 keys 声明自己
|
||||||
|
* 包含哪些字段。这里是纯函数,方便在 Node 契约测试里直接用真实 schema 校验,
|
||||||
|
* 避免渲染层的一个小疏忽让整块手风琴静默退化成平铺。
|
||||||
|
*/
|
||||||
|
export function buildFieldsetSections(schema) {
|
||||||
|
const declared = schema && Array.isArray(schema.sections) ? schema.sections : []
|
||||||
|
if (!declared.length) return []
|
||||||
|
|
||||||
|
const byKey = {}
|
||||||
|
const nested = Array.isArray(schema.fields) ? schema.fields : []
|
||||||
|
for (const field of nested) {
|
||||||
|
if (field && field.key) byKey[field.key] = field
|
||||||
|
}
|
||||||
|
|
||||||
|
return declared.map((section) => ({
|
||||||
|
title: String((section && section.title) || ''),
|
||||||
|
fields: (Array.isArray(section && section.keys) ? section.keys : [])
|
||||||
|
.map((key) => byKey[key])
|
||||||
|
.filter(Boolean),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { renderFaIcon, schemaPage, customPage } from './helpers'
|
import { renderFaIcon, schemaPage } from './helpers'
|
||||||
|
|
||||||
export const extensionsSection = {
|
export const extensionsSection = {
|
||||||
key: 'extensions',
|
key: 'extensions',
|
||||||
@@ -42,7 +42,7 @@ export const extensionsSection = {
|
|||||||
label: '百度推送',
|
label: '百度推送',
|
||||||
route: '/extensions/push',
|
route: '/extensions/push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'push',
|
section: 'push',
|
||||||
}),
|
}),
|
||||||
@@ -56,15 +56,5 @@ export const extensionsSection = {
|
|||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'security',
|
section: 'security',
|
||||||
}),
|
}),
|
||||||
customPage({
|
|
||||||
key: 'extensions-ai',
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
label: 'AI 连接配置',
|
|
||||||
route: '/extensions/ai',
|
|
||||||
component: 'ExtensionAI',
|
|
||||||
status: 'available',
|
|
||||||
title: 'AI 连接配置',
|
|
||||||
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -88,5 +88,16 @@ export const paymentSection = {
|
|||||||
canonicalDomain: 'payment',
|
canonicalDomain: 'payment',
|
||||||
section: 'withdrawal',
|
section: 'withdrawal',
|
||||||
}),
|
}),
|
||||||
|
schemaPage({
|
||||||
|
key: 'payment-after-sale',
|
||||||
|
name: 'PaymentAfterSale',
|
||||||
|
label: '售后策略',
|
||||||
|
route: '/payment/after-sale',
|
||||||
|
status: 'available',
|
||||||
|
title: '售后策略',
|
||||||
|
desc: '售后功能开关、申请时效、超时效提示与申请表单(预置原因、所需材料、图片上传)配置。',
|
||||||
|
canonicalDomain: 'payment',
|
||||||
|
section: 'after_sale',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -108,16 +108,6 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'sms',
|
section: 'sms',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'users-wechat',
|
|
||||||
name: 'UsersWechat',
|
|
||||||
label: '微信通知',
|
|
||||||
route: '/users/wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
canonicalDomain: 'users',
|
|
||||||
section: 'wechat',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'users-wechat-template',
|
key: 'users-wechat-template',
|
||||||
name: 'UsersWechatTemplate',
|
name: 'UsersWechatTemplate',
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
|
|
||||||
|
|
||||||
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
|
|
||||||
<strong>加载失败:</strong>{{ loadError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__status">
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
|
||||||
<i class="fa fab fa-android"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">可用状态</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-plug"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">WP AI Client</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-key"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">主题 Provider</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
|
||||||
<i class="fa fas fa-road"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">当前后端</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>主题 Provider 配置(回退链路)</h3>
|
|
||||||
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>AI Provider</label>
|
|
||||||
<select v-model="form.provider" class="aksrt-select">
|
|
||||||
<option value="none">不使用</option>
|
|
||||||
<option value="openai">OpenAI 兼容接口</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>API Key</label>
|
|
||||||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
|
||||||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
|
||||||
<label>API Base URL</label>
|
|
||||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://llm.example.com/v1">
|
|
||||||
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>模型</label>
|
|
||||||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>最大 Token 数</label>
|
|
||||||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>每分钟请求限制</label>
|
|
||||||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
|
||||||
{{ saving ? '保存中...' : '保存设置' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>已接通能力</h3>
|
|
||||||
<table class="aksrt-table aksrt-table--sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>能力</th>
|
|
||||||
<th>入口</th>
|
|
||||||
<th>状态</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="feat in features" :key="feat.key">
|
|
||||||
<td>{{ feat.name }}</td>
|
|
||||||
<td>{{ feat.scene }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
|
||||||
{{ feat.ready ? '可用' : '未就绪' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
|
||||||
<h3><i class="fa fas fa-exclamation-triangle"></i> 重要说明</h3>
|
|
||||||
<ul>
|
|
||||||
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
|
|
||||||
<li>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
|
|
||||||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
|
||||||
import PageHeader from '@/components/PageHeader.vue';
|
|
||||||
import api from '@/api';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
components: { PageHeader },
|
|
||||||
setup() {
|
|
||||||
const status = ref({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
configured: false,
|
|
||||||
model: '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: 10,
|
|
||||||
available: false,
|
|
||||||
backend: 'none',
|
|
||||||
wp_ai_available: false,
|
|
||||||
theme_available: false,
|
|
||||||
seo_ai_enabled: false,
|
|
||||||
write_assist: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
api_key: '',
|
|
||||||
api_base: 'https://api.openai.com/v1',
|
|
||||||
model: 'gpt-3.5-turbo',
|
|
||||||
max_tokens: 1000,
|
|
||||||
rate_limit: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const saving = ref(false);
|
|
||||||
const loadError = ref('');
|
|
||||||
|
|
||||||
const backendLabel = computed(() => {
|
|
||||||
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
|
|
||||||
return map[status.value.backend] || status.value.backend || '无';
|
|
||||||
});
|
|
||||||
|
|
||||||
const features = computed(() => {
|
|
||||||
const avail = !!status.value.available;
|
|
||||||
const seoReady = avail && !!status.value.seo_ai_enabled;
|
|
||||||
return [
|
|
||||||
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
|
|
||||||
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
|
|
||||||
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function hydrateFromStatus(data) {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
status.value = {
|
|
||||||
enabled: !!data.enabled,
|
|
||||||
provider: data.provider || 'none',
|
|
||||||
configured: !!data.configured,
|
|
||||||
model: data.model || '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: data.rate_limit || 10,
|
|
||||||
available: !!data.available,
|
|
||||||
backend: data.backend || 'none',
|
|
||||||
wp_ai_available: !!data.wp_ai_available,
|
|
||||||
theme_available: !!data.theme_available,
|
|
||||||
seo_ai_enabled: !!data.seo_ai_enabled,
|
|
||||||
write_assist: !!data.write_assist,
|
|
||||||
};
|
|
||||||
form.enabled = !!data.enabled;
|
|
||||||
form.provider = data.provider || 'none';
|
|
||||||
form.model = data.model || 'gpt-3.5-turbo';
|
|
||||||
form.rate_limit = data.rate_limit || 10;
|
|
||||||
if (data.api_base) form.api_base = data.api_base;
|
|
||||||
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
|
||||||
form.max_tokens = data.max_tokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStatus() {
|
|
||||||
loadError.value = '';
|
|
||||||
try {
|
|
||||||
const json = await api.getAiStatus();
|
|
||||||
hydrateFromStatus(json.data || {});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Load AI status failed:', e);
|
|
||||||
loadError.value = e?.message || '加载 AI 状态失败';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSettings() {
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
enabled: form.enabled,
|
|
||||||
provider: form.provider,
|
|
||||||
api_base: form.api_base,
|
|
||||||
model: form.model,
|
|
||||||
max_tokens: form.max_tokens,
|
|
||||||
rate_limit: form.rate_limit,
|
|
||||||
};
|
|
||||||
// Only send api_key when the user entered a new one.
|
|
||||||
if (form.api_key && form.api_key !== '***') {
|
|
||||||
payload.api_key = form.api_key;
|
|
||||||
}
|
|
||||||
const json = await api.updateAiSettings(payload);
|
|
||||||
alert(json.message || '设置已保存');
|
|
||||||
await loadStatus();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Save failed:', e);
|
|
||||||
alert(e?.message || '保存失败');
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadStatus());
|
|
||||||
|
|
||||||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-ai__status {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
|
|
||||||
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
|
|
||||||
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: var(--aksrt-color-text); }
|
|
||||||
.aksrt-ai__status-label { font-size: 12px; color: #888; }
|
|
||||||
.aksrt-ai__form-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
|
|
||||||
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
|
||||||
.aksrt-ai__load-error {
|
|
||||||
background: #fdecea;
|
|
||||||
border-color: #f5c2c0;
|
|
||||||
color: #842029;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
|
|
||||||
.aksrt-ai__notice h3 { color: #e65100; }
|
|
||||||
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
|
|
||||||
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
|
|
||||||
.aksrt-ai__notice code {
|
|
||||||
background: rgba(0,0,0,0.06);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
|
|
||||||
.aksrt-badge--success { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
|
||||||
.aksrt-form-group { margin-bottom: 14px; }
|
|
||||||
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
|
|
||||||
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
|
|
||||||
.aksrt-button--primary { background: #1677ff; color: #fff; }
|
|
||||||
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
|
|
||||||
</style>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const extensionsPageLoaders = {
|
|
||||||
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { corePageLoaders } from './core'
|
import { corePageLoaders } from './core'
|
||||||
import { usersPageLoaders } from './users'
|
import { usersPageLoaders } from './users'
|
||||||
import { toolsPageLoaders } from './tools'
|
import { toolsPageLoaders } from './tools'
|
||||||
import { extensionsPageLoaders } from './extensions'
|
|
||||||
|
|
||||||
export const componentLoaders = {
|
export const componentLoaders = {
|
||||||
...corePageLoaders,
|
...corePageLoaders,
|
||||||
...usersPageLoaders,
|
...usersPageLoaders,
|
||||||
...toolsPageLoaders,
|
...toolsPageLoaders,
|
||||||
...extensionsPageLoaders,
|
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,16 @@ export const pushSchema = {
|
|||||||
key: 'extensions.push',
|
key: 'extensions.push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
icon: 'fa fas fa-paper-plane',
|
icon: 'fa fas fa-paper-plane',
|
||||||
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
description: '配置百度主动推送、收录通道和推送状态。',
|
||||||
defaults: {
|
defaults: {
|
||||||
baidu_push_enabled: false,
|
baidu_push_enabled: false,
|
||||||
baidu_push_token: '',
|
baidu_push_token: '',
|
||||||
baidu_push_auto: true,
|
baidu_push_auto: true,
|
||||||
baidu_push_on_publish: true,
|
baidu_push_on_publish: true,
|
||||||
baidu_push_on_update: false,
|
baidu_push_on_update: false,
|
||||||
baidu_push_batch_size: 50,
|
baidu_push_normal: true,
|
||||||
|
baidu_push_fast: false,
|
||||||
baidu_push_log_enabled: true,
|
baidu_push_log_enabled: true,
|
||||||
xzh_on: false,
|
|
||||||
xzh_appid: '',
|
|
||||||
xzh_render_tail: true,
|
|
||||||
xzh_jsonld_single: true,
|
|
||||||
xzh_jsonld_page: false,
|
|
||||||
xzh_jsonld_img: false,
|
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
@@ -25,7 +20,7 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-rocket',
|
icon: 'fa fas fa-rocket',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
||||||
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' },
|
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,21 +29,17 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-bolt',
|
icon: 'fa fas fa-bolt',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
||||||
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' },
|
||||||
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'xzh',
|
key: 'channel',
|
||||||
title: '熊掌号 / JSON-LD',
|
title: '收录通道',
|
||||||
icon: 'fa fas fa-paw',
|
icon: 'fa fas fa-inbox',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
|
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
|
||||||
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
|
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
|
||||||
{ key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,8 +47,7 @@ export const pushSchema = {
|
|||||||
title: '高级设置',
|
title: '高级设置',
|
||||||
icon: 'fa fas fa-cog',
|
icon: 'fa fas fa-cog',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
|
||||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const seoSchema = {
|
|||||||
key: 'global.seo',
|
key: 'global.seo',
|
||||||
title: 'SEO 优化',
|
title: 'SEO 优化',
|
||||||
icon: 'fa fas fa-search',
|
icon: 'fa fas fa-search',
|
||||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框;连接配置在「扩展 → AI 连接配置」。优先使用 WordPress AI Connectors。',
|
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框,连接由 WordPress AI Connectors 提供。',
|
||||||
defaults: {
|
defaults: {
|
||||||
seo_enabled: true,
|
seo_enabled: true,
|
||||||
seo_ai_enabled: false,
|
seo_ai_enabled: false,
|
||||||
@@ -13,10 +13,6 @@ export const seoSchema = {
|
|||||||
ai_seo_title_prompt: '',
|
ai_seo_title_prompt: '',
|
||||||
ai_seo_keywords_prompt: '',
|
ai_seo_keywords_prompt: '',
|
||||||
ai_seo_description_prompt: '',
|
ai_seo_description_prompt: '',
|
||||||
seo_baidu_push: false,
|
|
||||||
seo_baidu_normal: false,
|
|
||||||
seo_baidu_fast: false,
|
|
||||||
seo_baidu_token: '',
|
|
||||||
seo_image_optimize: true,
|
seo_image_optimize: true,
|
||||||
seo_image_default: '',
|
seo_image_default: '',
|
||||||
seo_image_always_default: false,
|
seo_image_always_default: false,
|
||||||
@@ -48,7 +44,7 @@ export const seoSchema = {
|
|||||||
label: '启用 AI SEO 生成',
|
label: '启用 AI SEO 生成',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
default: false,
|
default: false,
|
||||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。',
|
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先在 WordPress 配置 AI Connectors。',
|
||||||
dependsOn: { key: 'seo_enabled', value: true },
|
dependsOn: { key: 'seo_enabled', value: true },
|
||||||
},
|
},
|
||||||
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
||||||
@@ -95,17 +91,6 @@ export const seoSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'baidu',
|
|
||||||
title: '百度推送',
|
|
||||||
icon: 'fa fas fa-paper-plane',
|
|
||||||
fields: [
|
|
||||||
{ key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'image',
|
key: 'image',
|
||||||
title: '图片 SEO',
|
title: '图片 SEO',
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export const affiliateSchema = {
|
|||||||
pay_rebate_poster_img: '',
|
pay_rebate_poster_img: '',
|
||||||
pay_rebate_poster_title: '发现一个精品网站,快来看一看',
|
pay_rebate_poster_title: '发现一个精品网站,快来看一看',
|
||||||
pay_rebate_poster_desc: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦',
|
pay_rebate_poster_desc: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦',
|
||||||
|
pay_rebate_poster_banner: '',
|
||||||
|
pay_rebate_poster_desc_bottom: '',
|
||||||
pay_rebate_text_discount: '推荐购买 下单再减%discount%',
|
pay_rebate_text_discount: '推荐购买 下单再减%discount%',
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
@@ -173,6 +175,8 @@ export const affiliateSchema = {
|
|||||||
{ key: 'pay_rebate_poster_img', label: '推广海报顶部图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
|
{ key: 'pay_rebate_poster_img', label: '推广海报顶部图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
|
||||||
{ key: 'pay_rebate_poster_title', label: '推广海报标题(20-30个字)', type: 'input', default: '发现一个精品网站,快来看一看', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
|
{ key: 'pay_rebate_poster_title', label: '推广海报标题(20-30个字)', type: 'input', default: '发现一个精品网站,快来看一看', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
|
||||||
{ key: 'pay_rebate_poster_desc', label: '推广海报简介(40-60个字)', type: 'input', default: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
|
{ key: 'pay_rebate_poster_desc', label: '推广海报简介(40-60个字)', type: 'input', default: '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', dependsOn: { key: 'pay_rebate_poster_s', value: true } },
|
||||||
|
{ key: 'pay_rebate_poster_banner', label: '海报顶部备用图片', type: 'media', default: '', dependsOn: { key: 'pay_rebate_poster_s', value: true }, help: '顶部主图加载失败时依次回退到这里,再回退到主题内置图,避免海报顶部出现空白。' },
|
||||||
|
{ key: 'pay_rebate_poster_desc_bottom', label: '海报底部文案', type: 'input', default: '扫描二维码阅读全文', dependsOn: { key: 'pay_rebate_poster_s', value: true }, help: '画在二维码下方居中位置,留空则使用默认文案。' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 售后策略 — 售后开关、申请时效、超时效提示与申请表单配置。
|
||||||
|
*
|
||||||
|
* 字段键、顺序、类型与默认值需与 src/Admin/Schema/SchemaPayment.php 的
|
||||||
|
* after_sale 分区保持 1:1(tests/widget-system/after-sale-policy-contract.test.cjs 守护)。
|
||||||
|
*/
|
||||||
|
export const afterSaleSchema = {
|
||||||
|
key: 'payment.after_sale',
|
||||||
|
title: '售后策略',
|
||||||
|
icon: 'fa fas fa-undo-alt',
|
||||||
|
description: '售后功能开关、申请时效、超时效提示与申请表单(预置原因、申请说明、所需材料、图片上传)配置。',
|
||||||
|
defaults: {
|
||||||
|
after_sale_s: true,
|
||||||
|
after_sale_time_limit: 7,
|
||||||
|
after_sale_expired_text: '<div>抱歉,您的订单已超过售后申请时效</div>',
|
||||||
|
after_sale_reasons: [
|
||||||
|
{ t: '质量问题' },
|
||||||
|
{ t: '假冒品牌' },
|
||||||
|
{ t: '其他原因' },
|
||||||
|
],
|
||||||
|
after_sale_desc: '',
|
||||||
|
after_sale_materials: [
|
||||||
|
{ t: '联系邮箱' },
|
||||||
|
{ t: '联系电话' },
|
||||||
|
],
|
||||||
|
after_sale_reason_placeholder: '必填,请您详细填写申请原因',
|
||||||
|
after_sale_img_s: true,
|
||||||
|
},
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
key: 'main',
|
||||||
|
title: '售后功能',
|
||||||
|
icon: 'fa fas fa-undo-alt',
|
||||||
|
fields: [
|
||||||
|
{ key: 'after_sale_s', label: '售后功能', type: 'switch', default: true, help: '开启后用户可以申请售后;关闭后不再处理售后,前台「我的订单」不再显示申请售后按钮' },
|
||||||
|
{ key: 'after_sale_time_limit', label: '申请售后时效(天)', type: 'number', default: 7, min: 0, max: 3650, help: '订单支付后多少天内可以申请售后,超过时效将无法申请;0 表示不限制' },
|
||||||
|
{ key: 'after_sale_expired_text', label: '超过时效提示', type: 'textarea', sanitize_type: 'html', default: '<div>抱歉,您的订单已超过售后申请时效</div>', rows: 3, help: '订单超过售后时效后,用户点击申请售后时弹窗提示的内容(支持HTML代码,请注意代码规范)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'form',
|
||||||
|
title: '申请表单',
|
||||||
|
icon: 'fa fas fa-file-alt',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'after_sale_reasons',
|
||||||
|
label: '预置原因',
|
||||||
|
type: 'group',
|
||||||
|
button_title: '添加原因',
|
||||||
|
help: '用户申请售后时可选择的原因',
|
||||||
|
default: [
|
||||||
|
{ t: '质量问题' },
|
||||||
|
{ t: '假冒品牌' },
|
||||||
|
{ t: '其他原因' },
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: 't', label: '原因', type: 'input', default: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'after_sale_desc', label: '申请售后说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 5, help: '用户申请售后时显示的内容(支持HTML代码,请注意代码规范)' },
|
||||||
|
{
|
||||||
|
key: 'after_sale_materials',
|
||||||
|
label: '申请所需材料',
|
||||||
|
type: 'group',
|
||||||
|
button_title: '添加材料',
|
||||||
|
help: '用户提交售后申请时需要提供的资料,如联系邮箱、联系电话;此处的信息收集,仅用于售后联系与审核',
|
||||||
|
default: [
|
||||||
|
{ t: '联系邮箱' },
|
||||||
|
{ t: '联系电话' },
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: 't', label: '资料项', type: 'input', default: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'after_sale_reason_placeholder', label: '申请说明占位文案', type: 'input', default: '必填,请您详细填写申请原因', help: '前台申请售后弹窗中「申请说明」输入框的提示文字' },
|
||||||
|
{ key: 'after_sale_img_s', label: '申请售后可上传图片', type: 'switch', default: true, help: '开启后申请售后弹窗中出现图片上传入口(最多上传3张)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { revenueSchema } from './revenue.js'
|
|||||||
import { affiliateSchema } from './affiliate.js'
|
import { affiliateSchema } from './affiliate.js'
|
||||||
import { gatewaySchema } from './gateway.js'
|
import { gatewaySchema } from './gateway.js'
|
||||||
import { withdrawalSchema } from './withdrawal.js'
|
import { withdrawalSchema } from './withdrawal.js'
|
||||||
|
import { afterSaleSchema } from './afterSale.js'
|
||||||
|
|
||||||
export const paymentSchemas = {
|
export const paymentSchemas = {
|
||||||
knowledge: knowledgeSchema,
|
knowledge: knowledgeSchema,
|
||||||
@@ -18,4 +19,5 @@ export const paymentSchemas = {
|
|||||||
affiliate: affiliateSchema,
|
affiliate: affiliateSchema,
|
||||||
gateway: gatewaySchema,
|
gateway: gatewaySchema,
|
||||||
withdrawal: withdrawalSchema,
|
withdrawal: withdrawalSchema,
|
||||||
|
after_sale: afterSaleSchema,
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,6 @@ export const knowledgeSchema = {
|
|||||||
payment_order_status_closed: '已关闭',
|
payment_order_status_closed: '已关闭',
|
||||||
pay_service: [],
|
pay_service: [],
|
||||||
down_alone_page: false,
|
down_alone_page: false,
|
||||||
down_user_log: 50,
|
|
||||||
down_post_log: 50,
|
|
||||||
down_remote_to_local_s: true,
|
down_remote_to_local_s: true,
|
||||||
down_remote_to_local_size: 2048,
|
down_remote_to_local_size: 2048,
|
||||||
down_remote_to_local_ext: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf',
|
down_remote_to_local_ext: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf',
|
||||||
@@ -123,7 +121,7 @@ export const knowledgeSchema = {
|
|||||||
{ key: 'pay_mark', label: '付费标记', type: 'input', default: '付费' },
|
{ key: 'pay_mark', label: '付费标记', type: 'input', default: '付费' },
|
||||||
{ key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
|
{ key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
|
||||||
{ key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
|
{ key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
|
||||||
{ key: 'payment_show_order_no', label: '显示流水号', type: 'switch', default: true },
|
{ key: 'payment_show_order_no', label: '显示订单号', type: 'switch', default: true },
|
||||||
{ key: 'payment_show_create_time', label: '显示创建时间', type: 'switch', default: true },
|
{ key: 'payment_show_create_time', label: '显示创建时间', type: 'switch', default: true },
|
||||||
{ key: 'payment_show_pay_time', label: '显示支付时间', type: 'switch', default: true },
|
{ key: 'payment_show_pay_time', label: '显示支付时间', type: 'switch', default: true },
|
||||||
],
|
],
|
||||||
@@ -172,8 +170,6 @@ export const knowledgeSchema = {
|
|||||||
title: '付费下载',
|
title: '付费下载',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'down_alone_page', label: '独立下载页面', type: 'switch', default: false, help: '开启后,付费资源的下载链接会跳转独立页面显示' },
|
{ key: 'down_alone_page', label: '独立下载页面', type: 'switch', default: false, help: '开启后,付费资源的下载链接会跳转独立页面显示' },
|
||||||
{ key: 'down_user_log', label: '保存用户下载记录(条)', type: 'number', default: 50, min: 0, help: '填0则不保存,注意记录量太大可能会影响网站性能' },
|
|
||||||
{ key: 'down_post_log', label: '保存文章下载记录(条)', type: 'number', default: 50, min: 0, help: '填0则不保存,注意记录量太大可能会影响网站性能' },
|
|
||||||
{ key: 'down_remote_to_local_s', label: '远程文件转本地文件下载', type: 'switch', default: true, help: '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址' },
|
{ key: 'down_remote_to_local_s', label: '远程文件转本地文件下载', type: 'switch', default: true, help: '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址' },
|
||||||
{ key: 'down_remote_to_local_size', label: '文件大小限制(Kb)', type: 'number', default: 2048, min: 0 },
|
{ key: 'down_remote_to_local_size', label: '文件大小限制(Kb)', type: 'number', default: 2048, min: 0 },
|
||||||
{ key: 'down_remote_to_local_ext', label: '文件格式限制', type: 'input', default: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', help: '限制使用此功能的文件格式,多个用英文逗号隔开' },
|
{ key: 'down_remote_to_local_ext', label: '文件格式限制', type: 'input', default: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', help: '限制使用此功能的文件格式,多个用英文逗号隔开' },
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const rechargeSchema = {
|
|||||||
{ key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
{ key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||||
{ key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' },
|
{ key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' },
|
||||||
{ key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' },
|
{ key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' },
|
||||||
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||||
{ key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)' },
|
{ key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,27 +1,127 @@
|
|||||||
/**
|
/**
|
||||||
* VIP会员 — 参考主题结构:会员等级、会员权益(普通/一级/二级 tab)、
|
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
|
||||||
* 会员购买选项(一级/二级/续费/升级 tab)、卡密兑换与积分兑换。
|
* (普通/一级/二级 tab)、会员购买选项(一级/二级/续费/升级 tab)、
|
||||||
|
* 积分兑换会员与界面文案。
|
||||||
|
*
|
||||||
|
* 会员商品 / 续费商品 / 升级价格 / 积分兑换明细均为结构化 group 或
|
||||||
|
* accordion,不再使用 `|` 分隔的 textarea。等级 Tab 标题通过 titleKey
|
||||||
|
* 绑定「会员显示名称」,改名即改 Tab 名。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const VIP_EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||||
|
|
||||||
|
const vipTagPalette = [
|
||||||
|
{ label: '#f74b3d', value: 'b-red' },
|
||||||
|
{ label: '#f3920a', value: 'b-yellow' },
|
||||||
|
{ label: '#08c4c1', value: 'b-cyan' },
|
||||||
|
{ label: '#0a8cf3', value: 'b-blue' },
|
||||||
|
{ label: '#1fd05a', value: 'b-green' },
|
||||||
|
{ label: '#c133f5', value: 'b-purple' },
|
||||||
|
{ label: '#121517', value: 'b-black' },
|
||||||
|
{ label: 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)', value: 'jb-red' },
|
||||||
|
{ label: 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)', value: 'jb-pink' },
|
||||||
|
{ label: 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)', value: 'jb-yellow' },
|
||||||
|
{ label: 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)', value: 'jb-cyan' },
|
||||||
|
{ label: 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)', value: 'jb-blue' },
|
||||||
|
{ label: 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)', value: 'jb-green' },
|
||||||
|
{ label: 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)', value: 'jb-purple' },
|
||||||
|
{ label: 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)', value: 'jb-vip1' },
|
||||||
|
{ label: 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)', value: 'jb-vip2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const vipUnitOptions = [
|
||||||
|
{ label: '天', value: 'day' },
|
||||||
|
{ label: '月', value: 'month' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||||
|
const vipProductGroupOptions = {
|
||||||
|
collapsible: true,
|
||||||
|
defaultCollapsed: true,
|
||||||
|
listPreview: { prefix: '价格:', titleKey: 'price', descKey: 'time', descUnitKey: 'unit' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 积分兑换明细的手风琴配置:默认收起,标题行显示积分与时长摘要。
|
||||||
|
const vipPointsGroupOptions = {
|
||||||
|
collapsible: true,
|
||||||
|
defaultCollapsed: true,
|
||||||
|
listPreview: { prefix: '积分:', titleKey: 'points', descKey: 'time', descUnitKey: 'unit' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员商品字段集(参考主题 CFS_Module::vip_product)。
|
||||||
|
*/
|
||||||
|
function vipProductFields() {
|
||||||
|
return [
|
||||||
|
{ key: 'price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '699.00', min: 0, suffix: '元', help: '会员的实际购买价格' },
|
||||||
|
{ key: 'show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '999.00', min: 0, suffix: '元', help: '显示在执行价格前面,并划掉' },
|
||||||
|
{ key: 'tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||||
|
{ key: 'tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { key: 'tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||||
|
{ key: 'time', label: '会员有效时间', type: 'spinner', default: 3, min: 0, max: 3600, step: 1, help: '开通会员的时长。填0则为永久会员' },
|
||||||
|
{ key: 'unit', label: '时长单位', type: 'radio', default: 'month', inline: true, dependsOn: { key: 'time', operator: 'truthy' }, options: vipUnitOptions },
|
||||||
|
{ key: '_time_permanent_hint', label: '', type: 'alert', alertType: 'success', ui_only: true, dependsOn: { key: 'time', operator: 'eq', value: 0 }, content: '会员有效时间已设置为:永久会员' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员等级 Tab 字段集(参考主题 CFS_Module::vip_tab,显示名称并入 Tab)。
|
||||||
|
*/
|
||||||
|
function vipLevelTabFields(level) {
|
||||||
|
const dep = { key: `pay_user_vip_${level}_s`, value: true };
|
||||||
|
return [
|
||||||
|
{ key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', sanitize_type: 'html', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '支持HTML,请注意控制长度' },
|
||||||
|
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', sanitize_type: 'html', default: '尊享专属特权', dependsOn: dep, help: '支持HTML' },
|
||||||
|
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '支持安全HTML,建议使用 <li> 列表项' },
|
||||||
|
{ key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' },
|
||||||
|
{
|
||||||
|
key: `vip_${level}_product`,
|
||||||
|
label: '会员商品',
|
||||||
|
type: 'group',
|
||||||
|
max: 8,
|
||||||
|
button_title: '添加会员商品',
|
||||||
|
default: [
|
||||||
|
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
|
...vipProductGroupOptions,
|
||||||
|
dependsOn: { all: [dep, { key: `vip_${level}_product_s`, value: true }] },
|
||||||
|
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||||
|
fields: vipProductFields(),
|
||||||
|
},
|
||||||
|
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export const vipSchema = {
|
export const vipSchema = {
|
||||||
key: 'payment.vip',
|
key: 'payment.vip',
|
||||||
title: 'VIP会员',
|
title: 'VIP会员',
|
||||||
icon: 'fa fas fa-gem',
|
icon: 'fa fas fa-gem',
|
||||||
description: '双级会员:等级名称、权益(下载次数与限速)、套餐、续费/升级与积分兑换。',
|
description: '双级会员:等级开关、权益(下载次数与限速)、会员商品、续费/升级与积分兑换。',
|
||||||
defaults: {
|
defaults: {
|
||||||
nav_pay_vip: true,
|
nav_pay_vip: true,
|
||||||
nav_user_pay_vip: true,
|
nav_user_pay_vip: true,
|
||||||
pay_user_vip_1_s: true,
|
pay_user_vip_1_s: true,
|
||||||
pay_user_vip_1_name: '一级会员',
|
pay_user_vip_1_name: '黄金会员',
|
||||||
pay_user_vip_1_desc: '尊享一级会员特权',
|
pay_user_vip_1_desc: '尊享专属特权',
|
||||||
pay_user_vip_1_equity: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>',
|
pay_user_vip_1_equity: VIP_EQUITY_DEFAULT,
|
||||||
|
vip_1_product_s: true,
|
||||||
|
vip_1_product: [
|
||||||
|
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
vip_1_img_icon: '',
|
vip_1_img_icon: '',
|
||||||
pay_user_vip_2_s: true,
|
pay_user_vip_2_s: true,
|
||||||
pay_user_vip_2_name: '二级会员',
|
pay_user_vip_2_name: '钻石会员',
|
||||||
pay_user_vip_2_desc: '尊享二级会员特权',
|
pay_user_vip_2_desc: '尊享专属特权',
|
||||||
pay_user_vip_2_equity: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>',
|
pay_user_vip_2_equity: VIP_EQUITY_DEFAULT,
|
||||||
|
vip_2_product_s: true,
|
||||||
|
vip_2_product: [
|
||||||
|
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
vip_2_img_icon: '',
|
vip_2_img_icon: '',
|
||||||
pay_user_vip_desc: '',
|
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||||
pay_user_vip_more: '',
|
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||||
pay_download_limit: 0,
|
pay_download_limit: 0,
|
||||||
pay_download_speed: 0,
|
pay_download_speed: 0,
|
||||||
pay_download_limit_vip_1: 0,
|
pay_download_limit_vip_1: 0,
|
||||||
@@ -29,19 +129,45 @@ export const vipSchema = {
|
|||||||
pay_download_limit_vip_2: 0,
|
pay_download_limit_vip_2: 0,
|
||||||
pay_download_speed_vip_2: 0,
|
pay_download_speed_vip_2: 0,
|
||||||
vip_1_product_s: true,
|
vip_1_product_s: true,
|
||||||
payment_vip_1_products: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
|
||||||
vip_2_product_s: true,
|
|
||||||
payment_vip_2_products: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
|
||||||
vip_renew: true,
|
vip_renew: true,
|
||||||
vip_renew_desc: '',
|
vip_renew_desc: '立即续费会员,畅享VIP权益',
|
||||||
vip_renew_price_type: 'discount',
|
vip_renew_price_type: 'discount',
|
||||||
vip_renew_discount: 8,
|
vip_renew_discount: 8,
|
||||||
vip_renew_reduce: '20.00',
|
vip_renew_reduce: '20.00',
|
||||||
|
vip_1_renew_product: [
|
||||||
|
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
|
vip_2_renew_product: [
|
||||||
|
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
|
vip_2_product_s: true,
|
||||||
|
vip_renew: true,
|
||||||
|
vip_renew_discount: 8,
|
||||||
|
vip_renew_reduce: '20.00',
|
||||||
vip_upgrade: true,
|
vip_upgrade: true,
|
||||||
vip_upgrade_desc: '',
|
vip_upgrade_desc: '升级VIP会员,享更多会员权益',
|
||||||
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
vip_upgrade_product: {
|
||||||
|
month_to_month_s: true,
|
||||||
|
unit_price: '10.00',
|
||||||
|
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||||
|
unit_tag_class: 'jb-yellow',
|
||||||
|
jump_s: false,
|
||||||
|
jump_price: '199.00',
|
||||||
|
jump_show_price: '299.00',
|
||||||
|
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||||
|
jump_tag_class: 'jb-red',
|
||||||
|
permanent_price: '199.00',
|
||||||
|
permanent_show_price: '299.00',
|
||||||
|
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||||
|
permanent_tag_class: 'jb-red',
|
||||||
|
},
|
||||||
pay_vip_points_exchange_s: false,
|
pay_vip_points_exchange_s: false,
|
||||||
payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
pay_vip_points_exchange_product: [
|
||||||
|
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||||
|
{ points: 5000, level: 1, time: 'Permanent' },
|
||||||
|
],
|
||||||
pay_vip_points_exchange_desc: '',
|
pay_vip_points_exchange_desc: '',
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
@@ -67,17 +193,16 @@ export const vipSchema = {
|
|||||||
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
||||||
{ key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' },
|
{ key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' },
|
||||||
{ key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true },
|
{ key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true },
|
||||||
{ key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '一级会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
{
|
||||||
{ key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享一级会员特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
key: 'vip_level_order_notice',
|
||||||
{ key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', default: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用 <li> 列表项' },
|
label: '',
|
||||||
{ key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' },
|
type: 'notice',
|
||||||
|
ui_only: true,
|
||||||
|
alertType: 'danger',
|
||||||
|
dependsOn: { all: [{ key: 'pay_user_vip_2_s', value: true }, { key: 'pay_user_vip_1_s', operator: 'falsy' }] },
|
||||||
|
content: '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>',
|
||||||
|
},
|
||||||
{ key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true },
|
{ key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true },
|
||||||
{ key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '二级会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
|
||||||
{ key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享二级会员特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
|
||||||
{ key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', default: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
|
||||||
{ key: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '留空使用内置 vip-2.svg 图标' },
|
|
||||||
{ key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' },
|
|
||||||
{ key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,6 +226,8 @@ export const vipSchema = {
|
|||||||
{
|
{
|
||||||
key: 'vip1',
|
key: 'vip1',
|
||||||
title: '一级会员',
|
title: '一级会员',
|
||||||
|
titleKey: 'pay_user_vip_1_name',
|
||||||
|
titleFallback: '一级会员',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'pay_download_limit_vip_1', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '一级会员每日最多可下载几个免费资源(为0则不限制)' },
|
{ key: 'pay_download_limit_vip_1', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '一级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||||
{ key: 'pay_download_speed_vip_1', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
{ key: 'pay_download_speed_vip_1', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||||
@@ -109,6 +236,8 @@ export const vipSchema = {
|
|||||||
{
|
{
|
||||||
key: 'vip2',
|
key: 'vip2',
|
||||||
title: '二级会员',
|
title: '二级会员',
|
||||||
|
titleKey: 'pay_user_vip_2_name',
|
||||||
|
titleFallback: '二级会员',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'pay_download_limit_vip_2', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '二级会员每日最多可下载几个免费资源(为0则不限制)' },
|
{ key: 'pay_download_limit_vip_2', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '二级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||||
{ key: 'pay_download_speed_vip_2', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
{ key: 'pay_download_speed_vip_2', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||||
@@ -131,53 +260,111 @@ export const vipSchema = {
|
|||||||
{
|
{
|
||||||
key: 'vip1',
|
key: 'vip1',
|
||||||
title: '一级会员',
|
title: '一级会员',
|
||||||
|
titleKey: 'pay_user_vip_1_name',
|
||||||
|
titleFallback: '一级会员',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
...vipLevelTabFields(1),
|
||||||
{
|
|
||||||
key: 'payment_vip_1_products',
|
|
||||||
label: '一级会员套餐',
|
|
||||||
type: 'textarea',
|
|
||||||
default: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
|
||||||
dependsOn: { key: 'vip_1_product_s', value: true },
|
|
||||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'vip2',
|
key: 'vip2',
|
||||||
title: '二级会员',
|
title: '二级会员',
|
||||||
|
titleKey: 'pay_user_vip_2_name',
|
||||||
|
titleFallback: '二级会员',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
...vipLevelTabFields(2),
|
||||||
{
|
|
||||||
key: 'payment_vip_2_products',
|
|
||||||
label: '二级会员套餐',
|
|
||||||
type: 'textarea',
|
|
||||||
default: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
|
||||||
dependsOn: { key: 'vip_2_product_s', value: true },
|
|
||||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'renew',
|
key: 'renew',
|
||||||
title: '会员续费',
|
title: '会员续费',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_renew', label: '允许续费', type: 'switch', default: true, help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费' },
|
{
|
||||||
{ key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } },
|
key: 'vip_renew',
|
||||||
|
label: '允许续费',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vip_renew_desc',
|
||||||
|
label: '续费介绍',
|
||||||
|
type: 'input',
|
||||||
|
sanitize_type: 'html',
|
||||||
|
default: '立即续费会员,畅享VIP权益',
|
||||||
|
dependsOn: { key: 'vip_renew', value: true },
|
||||||
|
help: '续费会员的一句话简介,支持HTML',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'vip_renew_price_type',
|
key: 'vip_renew_price_type',
|
||||||
label: '续费价格类型',
|
label: '续费价格类型',
|
||||||
type: 'radio',
|
type: 'select',
|
||||||
default: 'discount',
|
default: 'discount',
|
||||||
dependsOn: { key: 'vip_renew', value: true },
|
dependsOn: { key: 'vip_renew', value: true },
|
||||||
options: [
|
options: [
|
||||||
{ label: '相同价格', value: 'same' },
|
{ label: '相同价格', value: 'same' },
|
||||||
{ label: '折扣价格', value: 'discount' },
|
{ label: '折扣价格', value: 'discount' },
|
||||||
{ label: '优惠立减', value: 'reduce' },
|
{ label: '优惠立减', value: 'reduce' },
|
||||||
|
{ label: '自定义商品', value: 'customize' },
|
||||||
],
|
],
|
||||||
|
help: '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vip_renew_discount',
|
||||||
|
label: '续费折扣',
|
||||||
|
type: 'slider',
|
||||||
|
sanitize_type: 'float',
|
||||||
|
default: 8,
|
||||||
|
min: 0.1,
|
||||||
|
max: 9.9,
|
||||||
|
step: 0.1,
|
||||||
|
unit: '折',
|
||||||
|
dependsOn: { key: 'vip_renew_price_type', value: 'discount' },
|
||||||
|
help: '在会员商品基础上打几折,如 8 = 八折',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vip_renew_reduce',
|
||||||
|
label: '续费减免金额',
|
||||||
|
type: 'spinner',
|
||||||
|
sanitize_type: 'money_decimal',
|
||||||
|
default: '20.00',
|
||||||
|
min: 0,
|
||||||
|
max: 500,
|
||||||
|
step: 5,
|
||||||
|
unit: '元',
|
||||||
|
dependsOn: { key: 'vip_renew_price_type', value: 'reduce' },
|
||||||
|
help: '在会员商品基础上优惠的金额,优惠后总金额不能为0元',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vip_1_renew_product',
|
||||||
|
label: '一级会员续费',
|
||||||
|
type: 'group',
|
||||||
|
max: 8,
|
||||||
|
button_title: '添加续费商品',
|
||||||
|
default: [
|
||||||
|
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
|
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||||
|
help: '一级会员续费的商品选项',
|
||||||
|
...vipProductGroupOptions,
|
||||||
|
fields: vipProductFields(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vip_2_renew_product',
|
||||||
|
label: '二级会员续费',
|
||||||
|
type: 'group',
|
||||||
|
max: 8,
|
||||||
|
button_title: '添加续费商品',
|
||||||
|
default: [
|
||||||
|
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||||
|
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||||
|
],
|
||||||
|
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||||
|
help: '二级会员续费的商品选项',
|
||||||
|
...vipProductGroupOptions,
|
||||||
|
fields: vipProductFields(),
|
||||||
},
|
},
|
||||||
{ key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } },
|
|
||||||
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'input', sanitize_type: 'money_decimal', default: '20.00', min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -185,14 +372,50 @@ export const vipSchema = {
|
|||||||
title: '会员升级',
|
title: '会员升级',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
|
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
|
||||||
{ key: 'vip_upgrade_desc', label: '升级说明', type: 'textarea', default: '', dependsOn: { key: 'vip_upgrade', value: true } },
|
{ key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', sanitize_type: 'html', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介,支持HTML' },
|
||||||
{
|
{
|
||||||
key: 'vip_upgrade_product',
|
key: 'vip_upgrade_product',
|
||||||
label: '升级产品',
|
label: '升级价格',
|
||||||
type: 'textarea',
|
type: 'accordion',
|
||||||
default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
default: {
|
||||||
|
month_to_month_s: true,
|
||||||
|
unit_price: '10.00',
|
||||||
|
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||||
|
unit_tag_class: 'jb-yellow',
|
||||||
|
jump_s: false,
|
||||||
|
jump_price: '199.00',
|
||||||
|
jump_show_price: '299.00',
|
||||||
|
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||||
|
jump_tag_class: 'jb-red',
|
||||||
|
permanent_price: '199.00',
|
||||||
|
permanent_show_price: '299.00',
|
||||||
|
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||||
|
permanent_tag_class: 'jb-red',
|
||||||
|
},
|
||||||
dependsOn: { key: 'vip_upgrade', value: true },
|
dependsOn: { key: 'vip_upgrade', value: true },
|
||||||
help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题',
|
help: '会员升级的商品选项',
|
||||||
|
sections: [
|
||||||
|
{ title: '月费会员升级月费会员', keys: ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class'] },
|
||||||
|
{ title: '月费会员升级永久会员', keys: ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class'] },
|
||||||
|
{ title: '永久会员升级永久会员', keys: ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class'] },
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: '_upgrade_unit_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价' },
|
||||||
|
{ key: 'month_to_month_s', label: '月费会员升级月费会员', type: 'switch', default: true },
|
||||||
|
{ key: 'unit_price', label: '天单价', type: 'number', sanitize_type: 'money_decimal', default: '10.00', min: 0, unit: '元', dependsOn: { key: 'month_to_month_s', value: true } },
|
||||||
|
{ key: 'unit_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', rows: 1, dependsOn: { key: 'month_to_month_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||||
|
{ key: 'unit_tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { all: [{ key: 'month_to_month_s', value: true }, { key: 'unit_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||||
|
{ key: '_upgrade_jump_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项' },
|
||||||
|
{ key: 'jump_s', label: '月费会员升级永久会员', type: 'switch', default: false },
|
||||||
|
{ key: 'jump_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '月费会员升级永久会员的价格' },
|
||||||
|
{ key: 'jump_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '显示在执行价格前面,并划掉' },
|
||||||
|
{ key: 'jump_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, dependsOn: { key: 'jump_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||||
|
{ key: 'jump_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { all: [{ key: 'jump_s', value: true }, { key: 'jump_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||||
|
{ key: 'permanent_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', help: '永久会员升级永久会员的价格' },
|
||||||
|
{ key: 'permanent_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', help: '显示在执行价格前面,并划掉' },
|
||||||
|
{ key: 'permanent_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||||
|
{ key: 'permanent_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { key: 'permanent_tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -205,16 +428,72 @@ export const vipSchema = {
|
|||||||
title: '积分兑换会员',
|
title: '积分兑换会员',
|
||||||
icon: 'fa fas fa-database',
|
icon: 'fa fas fa-database',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false },
|
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false, help: '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费' },
|
||||||
{
|
{
|
||||||
key: 'payment_vip_points_packages',
|
key: 'pay_vip_points_exchange_product',
|
||||||
label: '兑换套餐',
|
label: '积分兑换明细',
|
||||||
type: 'textarea',
|
type: 'group',
|
||||||
default: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
max: 16,
|
||||||
|
button_title: '添加积分兑换商品',
|
||||||
|
default: [
|
||||||
|
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||||
|
{ points: 5000, level: 1, time: 'Permanent' },
|
||||||
|
],
|
||||||
dependsOn: { key: 'pay_vip_points_exchange_s', value: true },
|
dependsOn: { key: 'pay_vip_points_exchange_s', value: true },
|
||||||
help: '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)',
|
help: '每一行为一个可兑换的会员套餐',
|
||||||
|
...vipPointsGroupOptions,
|
||||||
|
fields: [
|
||||||
|
{ key: 'points', label: '使用积分', type: 'number', default: 100, min: 0, unit: '积分' },
|
||||||
|
{
|
||||||
|
key: 'level',
|
||||||
|
label: '兑换会员',
|
||||||
|
type: 'radio',
|
||||||
|
default: 1,
|
||||||
|
inline: true,
|
||||||
|
options: [
|
||||||
|
{ label: '一级会员', value: 1 },
|
||||||
|
{ label: '二级会员', value: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'time', label: '兑换时长', type: 'input', default: '', help: '填 Permanent 为永久' },
|
||||||
|
{
|
||||||
|
key: 'unit',
|
||||||
|
label: '时长单位',
|
||||||
|
type: 'radio',
|
||||||
|
default: 'day',
|
||||||
|
inline: true,
|
||||||
|
dependsOn: { key: 'time', operator: 'neq', value: 'Permanent' },
|
||||||
|
options: vipUnitOptions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_points_exchange_s', value: true }, help: '显示在积分兑换弹窗底部,可以为提醒事项、兑换协议等,支持HTML代码' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'modal-copy',
|
||||||
|
title: '界面文案',
|
||||||
|
icon: 'fa fas fa-align-left',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'pay_user_vip_desc',
|
||||||
|
label: '会员介绍',
|
||||||
|
type: 'input',
|
||||||
|
sanitize_type: 'html',
|
||||||
|
default: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||||
|
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||||
|
help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍,支持HTML',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pay_user_vip_more',
|
||||||
|
label: '开通会员更多内容',
|
||||||
|
type: 'textarea',
|
||||||
|
sanitize_type: 'html',
|
||||||
|
default: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||||
|
rows: 4,
|
||||||
|
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||||
|
help: '显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码',
|
||||||
},
|
},
|
||||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export const permissionsSchemas = {
|
|||||||
{ label: '每日签到', value: 'signin' },
|
{ label: '每日签到', value: 'signin' },
|
||||||
{ label: '前台投稿', value: 'front_submit' },
|
{ label: '前台投稿', value: 'front_submit' },
|
||||||
{ label: '打赏用户', value: 'tip_user' },
|
{ label: '打赏用户', value: 'tip_user' },
|
||||||
|
{ label: '申请售后', value: 'after_sale_apply' },
|
||||||
|
{ label: '撤销售后', value: 'after_sale_cancel' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { featuresSchema } from './features.js'
|
|||||||
import { verifySchema } from './verify.js'
|
import { verifySchema } from './verify.js'
|
||||||
import { levelSchema } from './level.js'
|
import { levelSchema } from './level.js'
|
||||||
import { badgeSchema } from './badge.js'
|
import { badgeSchema } from './badge.js'
|
||||||
import { wechatSchema } from './wechat.js'
|
|
||||||
import { reportSchema } from './report.js'
|
import { reportSchema } from './report.js'
|
||||||
import { smsSchema } from './sms.js'
|
import { smsSchema } from './sms.js'
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export const usersSchemas = {
|
|||||||
verify: verifySchema,
|
verify: verifySchema,
|
||||||
level: levelSchema,
|
level: levelSchema,
|
||||||
badge: badgeSchema,
|
badge: badgeSchema,
|
||||||
wechat: wechatSchema,
|
|
||||||
report: reportSchema,
|
report: reportSchema,
|
||||||
sms: smsSchema,
|
sms: smsSchema,
|
||||||
}
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export const wechatSchema = {
|
|
||||||
key: 'users.wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
icon: 'fa fab fa-weixin',
|
|
||||||
description: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
defaults: {
|
|
||||||
wechat_notify_enabled: false,
|
|
||||||
wechat_app_id: '',
|
|
||||||
wechat_app_secret: '',
|
|
||||||
wechat_template_id: '',
|
|
||||||
wechat_token: '',
|
|
||||||
wechat_encoding_aes_key: '',
|
|
||||||
wechat_notify_comment: true,
|
|
||||||
wechat_notify_reply: true,
|
|
||||||
wechat_notify_system: true,
|
|
||||||
wechat_bind_entry: true,
|
|
||||||
},
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
key: 'account',
|
|
||||||
title: '公众号配置',
|
|
||||||
icon: 'fa fab fa-weixin',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_notify_enabled', label: '启用微信通知', type: 'switch', default: false, help: '开启后用户可绑定微信接收通知' },
|
|
||||||
{ key: 'wechat_app_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_template_id', label: '模板消息 ID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在微信公众平台申请的模板消息 ID' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'verify',
|
|
||||||
title: '服务器验证',
|
|
||||||
icon: 'fa fas fa-check-circle',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_token', label: 'Token', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '微信公众平台服务器配置中的 Token' },
|
|
||||||
{ key: 'wechat_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'triggers',
|
|
||||||
title: '通知触发器',
|
|
||||||
icon: 'fa fas fa-bell',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_notify_comment', label: '新评论通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_notify_reply', label: '评论回复通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_notify_system', label: '系统通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_bind_entry', label: '显示绑定入口', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在用户中心显示微信绑定/解绑按钮' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -668,95 +668,6 @@ code {
|
|||||||
|
|
||||||
/**---------------------通知结束-------------**/
|
/**---------------------通知结束-------------**/
|
||||||
|
|
||||||
/**模态框------*/
|
|
||||||
.aksrt-modal-backdrop,
|
|
||||||
.aksrt-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 100050;
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-backdrop {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal.in {
|
|
||||||
opacity: 1;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal.in .aksrt-modal-backdrop {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-dialog {
|
|
||||||
max-width: calc(100% - 20px);
|
|
||||||
margin: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
transition: transform 0.5s cubic-bezier(0.32, 0.85, 0.45, 1.18), width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-dialog.modal-mini {
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal.in .aksrt-modal-dialog {
|
|
||||||
transform: scale(1);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .close.abs-close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .border-title {
|
|
||||||
padding-bottom: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-content {
|
|
||||||
max-height: calc(100vh - 20px);
|
|
||||||
overflow: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-body {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .loading-mask {
|
|
||||||
z-index: 1;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .hide-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 6px;
|
|
||||||
font-size: 25px;
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**---------批量编辑------------**/
|
/**---------批量编辑------------**/
|
||||||
.aksrt-bulk-edit-box {
|
.aksrt-bulk-edit-box {
|
||||||
margin: -1px;
|
margin: -1px;
|
||||||
@@ -774,19 +685,18 @@ code {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**---------------TAB-栏目------------**/
|
.aksrt-bulk-edit-box select,
|
||||||
.aksrt-tab-pane {
|
.aksrt-bulk-edit-box input[type="text"] {
|
||||||
display: none;
|
max-width: 100%;
|
||||||
opacity: 0;
|
margin-right: 4px;
|
||||||
transition: 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-tab-pane.active {
|
.inline-edit-row fieldset .aksrt-bulk-edit-box {
|
||||||
display: block;
|
min-width: 168px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-tab-pane.in {
|
.aksrt-bulk-edit-desc {
|
||||||
opacity: 1;
|
color: #787c82;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**---------------后台设置项目-修改或隐藏-----------**/
|
/**---------------后台设置项目-修改或隐藏-----------**/
|
||||||
|
|||||||
@@ -709,8 +709,9 @@
|
|||||||
color: var(--color-primary, #f04494);
|
color: var(--color-primary, #f04494);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||||
.widget a.but {
|
.widget a.but,
|
||||||
|
.widget a.img-badge {
|
||||||
color: var(--this-color, #fff);
|
color: var(--this-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
|
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
|
||||||
+411
-354
@@ -916,6 +916,7 @@ strong, b {
|
|||||||
|
|
||||||
/* === Full width === */
|
/* === Full width === */
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,6 +962,8 @@ strong, b {
|
|||||||
.but > .fa:not(.fa-angle-right),
|
.but > .fa:not(.fa-angle-right),
|
||||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||||
|
|
||||||
|
.but.block { display: block; }
|
||||||
|
|
||||||
/* hover / active */
|
/* hover / active */
|
||||||
.but:focus,
|
.but:focus,
|
||||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||||
@@ -1092,7 +1095,7 @@ strong, b {
|
|||||||
|
|
||||||
/* gradient modifiers (legacy .jb-*) */
|
/* gradient modifiers (legacy .jb-*) */
|
||||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
||||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); }
|
.jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
|
||||||
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||||
@@ -2062,6 +2065,22 @@ html.dark .alert--info { color: #93C5FD; }
|
|||||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 8b. 头像形状统一 — 圆形基底
|
||||||
|
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||||
|
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||||
|
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||||
|
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||||
|
* ============================================================ */
|
||||||
|
img.avatar,
|
||||||
|
img.avatar-mini,
|
||||||
|
img.avatar-md,
|
||||||
|
img.avatar-lg,
|
||||||
|
.avatar-img img {
|
||||||
|
border-radius: var(--radius-avatar, 50%);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||||
@@ -2638,7 +2657,8 @@ main.container,
|
|||||||
.user-card.author .user-name > name {
|
.user-card.author .user-name > name {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.user-card.author .user-name > name > .display-name {
|
.user-card.author .user-name > name > .display-name,
|
||||||
|
.user-card.widget .user-name > name > .display-name {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -2688,31 +2708,35 @@ main.container,
|
|||||||
/**
|
/**
|
||||||
* Header account popover.
|
* Header account popover.
|
||||||
*
|
*
|
||||||
* The shared legacy navigation styles provide the popover shell. This module
|
* The shared legacy navigation styles provide the popover shell, and every
|
||||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||||
|
* through the reference legacy classes. This module only owns the navigation
|
||||||
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||||
.navbar-top .aksrt-navbar-actions {
|
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||||
display: flex;
|
* override only zeroes the horizontal margins, so every right-cluster group
|
||||||
align-items: center;
|
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||||
gap: 10px;
|
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||||
min-height: 50px;
|
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||||
margin-top: 0;
|
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||||
margin-bottom: 0;
|
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||||
padding: 0;
|
* its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
|
||||||
}
|
* min-height: it would double-count the 8px and push that group's content 8px
|
||||||
|
* below the line while inflating the navbar from 50px to 58px. */
|
||||||
.navbar-top .aksrt-navbar-actions > *,
|
.navbar-top .aksrt-navbar-account {
|
||||||
.navbar-top .aksrt-navbar-account,
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
.navbar-top .aksrt-navbar-account > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -2725,39 +2749,6 @@ main.container,
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account {
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: .3em 1em !important;
|
|
||||||
border-radius: var(--radius-btn, 4px);
|
|
||||||
font-size: .8em;
|
|
||||||
line-height: 1.44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-nav-icon,
|
.navbar-top .aksrt-nav-icon,
|
||||||
.navbar-top .aksrt-nav-avatar {
|
.navbar-top .aksrt-nav-avatar {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -2790,33 +2781,34 @@ main.container,
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
/* The account-row chips, the theme and the language toggles all want the 29px
|
||||||
|
* midline circle. The popover's quick-login buttons are .toggle-radius too and
|
||||||
|
* also live inside .navbar-form, but they must keep the .social_loginbar
|
||||||
|
* geometry from main.css: in a flex container flex-basis outranks width, so
|
||||||
|
* without this exemption the buttons collapse to 29px and lose the 5px side
|
||||||
|
* margins that space them apart. */
|
||||||
|
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
|
||||||
flex: 0 0 29px;
|
flex: 0 0 29px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-language {
|
/* Match the account row's 34px cross-size so the 29px toggle shares the 25px
|
||||||
|
* midline instead of sitting 2.5px above it. */
|
||||||
|
.navbar-top .navbar-form.flex.ac {
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||||
* the absolute panel's static position beside the compact CTA. */
|
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||||
position: relative;
|
* would move that static position to the flex container's cross-start. */
|
||||||
align-self: stretch;
|
.navbar-top .navbar-but > .hover-show {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
width: max-content;
|
|
||||||
max-width: calc(100vw - 24px);
|
|
||||||
margin: 0;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||||
@@ -2852,85 +2844,41 @@ main.container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox {
|
.aksrt-header-userbox {
|
||||||
width: 260px;
|
|
||||||
color: var(--color-ink-2);
|
color: var(--color-ink-2);
|
||||||
|
line-height: 1.42857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info {
|
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||||
min-height: 44px;
|
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||||
padding: 2px;
|
.aksrt-header-userbox .muted-box.padding-6 {
|
||||||
|
background-color: var(--this-bg, var(--color-line-soft));
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .avatar {
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
flex: 0 0 44px;
|
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||||
width: 44px;
|
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||||
height: 44px;
|
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||||
object-fit: cover;
|
* declarations are the last word in the cascade. Re-assert them here, which
|
||||||
|
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||||
|
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||||
|
.payvip-icon {
|
||||||
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
|
color: #ece0e0;
|
||||||
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right,
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
.aksrt-header-userbox .user-right > .flex1 {
|
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||||
min-width: 0;
|
display: block;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
.aksrt-header-userbox .user-right b {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .display-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right .img-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .abs-right {
|
|
||||||
top: 50%;
|
|
||||||
right: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .aksrt-notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
right: -4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||||
.aksrt-nav-notification .aksrt-notification-badge {
|
.aksrt-nav-notification .aksrt-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@@ -2950,222 +2898,20 @@ main.container,
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-wallet {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet:hover {
|
|
||||||
color: var(--color-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item i {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item b {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: #fff6db !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: #b78103 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin:hover {
|
|
||||||
background: #fff0c2 !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
|
||||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
|
||||||
.aksrt-header-checkin:disabled,
|
|
||||||
.aksrt-header-checkin:disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
opacity: .92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 7px 10px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__exp {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
box-shadow: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
opacity: .9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||||
* icon tiles and must opt out or their labels overlap the following row. */
|
* icon tiles and must opt out or their labels overlap the following row.
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
* The tile geometry itself stays with the shared legacy rules
|
||||||
align-items: flex-start;
|
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||||
gap: 4px;
|
* so the action row keeps the reference proportions. */
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||||
.aksrt-header-userbox .header-user-href > a {
|
.aksrt-header-userbox .header-user-href > a {
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 10px 2px 2px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex: 0 0 36px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-navbar .aksrt-header-userbox {
|
.mobile-navbar .aksrt-header-userbox {
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin-right: auto;
|
margin: auto;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin {
|
|
||||||
background: rgba(255, 193, 7, .18) !important;
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin > i {
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet {
|
|
||||||
background: rgba(255, 255, 255, .06);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet__item,
|
|
||||||
body.dark-theme .aksrt-header-wallet__item b {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.aksrt-header-userbox .header-user-href {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex-basis: 34px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* source: assets/css/frontend/card-primitives.css */
|
/* source: assets/css/frontend/card-primitives.css */
|
||||||
@@ -6429,14 +6175,15 @@ body.aksrt-modal-runtime-open {
|
|||||||
color: var(--color-ink, #111);
|
color: var(--color-ink, #111);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
|
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .add,
|
||||||
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -6452,14 +6199,14 @@ body.aksrt-modal-runtime-open {
|
|||||||
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
|
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
|
||||||
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
|
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
@@ -6474,7 +6221,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6490,6 +6237,316 @@ body.aksrt-modal-runtime-open {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 售后弹窗(申请 / 售后详细 / 超时效提示,参考主题 modal-mini 几何 1:1)----
|
||||||
|
三个弹窗与禁封申诉弹窗同几何:data_class='modal-mini'(宽 400px),圆角与
|
||||||
|
底色走 --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-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .aksrt-modal-runtime__skeleton {
|
||||||
|
padding: 24px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 蓝色炫彩标题区(参考主题 zib_get_modal_colorful_header('jb-blue', ...)):120px
|
||||||
|
绝对定位横幅 + 旋转彩斑,后续内容整体下移 120px,与禁封记录弹窗同构。 */
|
||||||
|
.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-withdraw-modal .colorful-bg,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--this-color);
|
||||||
|
background: var(--this-bg);
|
||||||
|
background-size: 120%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 + * {
|
||||||
|
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-withdraw-modal .modal-colorful-header > .close,
|
||||||
|
.aksrt-order-detail-modal .modal-colorful-header > .close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 9;
|
||||||
|
color: var(--this-color);
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
|
||||||
|
.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-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);
|
||||||
|
border-radius: 100%;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make {
|
||||||
|
top: 60%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 323px;
|
||||||
|
height: 290px;
|
||||||
|
animation: aksrt-ban-colorful 120s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before {
|
||||||
|
top: -325px;
|
||||||
|
right: -558px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 677px;
|
||||||
|
height: 300px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: rotate(341deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
||||||
|
top: -170px;
|
||||||
|
bottom: 0;
|
||||||
|
left: -262px;
|
||||||
|
width: 402px;
|
||||||
|
height: 214px;
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: rotate(326deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 售后原因徽章:与举报弹窗一致,可换行且间距走 mr6/mb6 工具类。 */
|
||||||
|
.aksrt-after-sale-apply-modal .form-but-radio .badg {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单摘要与售后明细行 */
|
||||||
|
.aksrt-after-sale-summary__row,
|
||||||
|
.aksrt-after-sale-detail__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-summary__row:last-child,
|
||||||
|
.aksrt-after-sale-detail__row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-summary__value,
|
||||||
|
.aksrt-after-sale-detail__value {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-detail__statusRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-reason-text {
|
||||||
|
color: var(--color-ink-2, #4e5358);
|
||||||
|
background: var(--surface-2, #eee);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-images img {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--color-line, #e5e7eb);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-status.is-pending { color: var(--color-warning, #f59e0b); }
|
||||||
|
.aksrt-after-sale-status.is-processing { color: var(--color-info, #3b82f6); }
|
||||||
|
.aksrt-after-sale-status.is-approved,
|
||||||
|
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
|
||||||
|
.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 */
|
/* source: assets/css/frontend/notyf.css */
|
||||||
/**
|
/**
|
||||||
* legacy-compatible frontend notifications.
|
* legacy-compatible frontend notifications.
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
|
|
||||||
/* === Full width === */
|
/* === Full width === */
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +183,8 @@
|
|||||||
.but > .fa:not(.fa-angle-right),
|
.but > .fa:not(.fa-angle-right),
|
||||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||||
|
|
||||||
|
.but.block { display: block; }
|
||||||
|
|
||||||
/* hover / active */
|
/* hover / active */
|
||||||
.but:focus,
|
.but:focus,
|
||||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||||
@@ -313,7 +316,7 @@
|
|||||||
|
|
||||||
/* gradient modifiers (legacy .jb-*) */
|
/* gradient modifiers (legacy .jb-*) */
|
||||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
||||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); }
|
.jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
|
||||||
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||||
|
|||||||
@@ -424,6 +424,22 @@
|
|||||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 8b. 头像形状统一 — 圆形基底
|
||||||
|
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||||
|
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||||
|
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||||
|
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||||
|
* ============================================================ */
|
||||||
|
img.avatar,
|
||||||
|
img.avatar-mini,
|
||||||
|
img.avatar-md,
|
||||||
|
img.avatar-lg,
|
||||||
|
.avatar-img img {
|
||||||
|
border-radius: var(--radius-avatar, 50%);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ main.container,
|
|||||||
.user-card.author .user-name > name {
|
.user-card.author .user-name > name {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.user-card.author .user-name > name > .display-name {
|
.user-card.author .user-name > name > .display-name,
|
||||||
|
.user-card.widget .user-name > name > .display-name {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* Header account popover.
|
* Header account popover.
|
||||||
*
|
*
|
||||||
* The shared legacy navigation styles provide the popover shell. This module
|
* The shared legacy navigation styles provide the popover shell, and every
|
||||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||||
|
* through the reference legacy classes. This module only owns the navigation
|
||||||
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||||
.navbar-top .aksrt-navbar-actions {
|
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||||
display: flex;
|
* override only zeroes the horizontal margins, so every right-cluster group
|
||||||
align-items: center;
|
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||||
gap: 10px;
|
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||||
min-height: 50px;
|
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||||
margin-top: 0;
|
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||||
margin-bottom: 0;
|
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||||
padding: 0;
|
* its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
|
||||||
}
|
* min-height: it would double-count the 8px and push that group's content 8px
|
||||||
|
* below the line while inflating the navbar from 50px to 58px. */
|
||||||
.navbar-top .aksrt-navbar-actions > *,
|
.navbar-top .aksrt-navbar-account {
|
||||||
.navbar-top .aksrt-navbar-account,
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
.navbar-top .aksrt-navbar-account > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -38,39 +42,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account {
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: .3em 1em !important;
|
|
||||||
border-radius: var(--radius-btn, 4px);
|
|
||||||
font-size: .8em;
|
|
||||||
line-height: 1.44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-nav-icon,
|
.navbar-top .aksrt-nav-icon,
|
||||||
.navbar-top .aksrt-nav-avatar {
|
.navbar-top .aksrt-nav-avatar {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -103,33 +74,34 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
/* The account-row chips, the theme and the language toggles all want the 29px
|
||||||
|
* midline circle. The popover's quick-login buttons are .toggle-radius too and
|
||||||
|
* also live inside .navbar-form, but they must keep the .social_loginbar
|
||||||
|
* geometry from main.css: in a flex container flex-basis outranks width, so
|
||||||
|
* without this exemption the buttons collapse to 29px and lose the 5px side
|
||||||
|
* margins that space them apart. */
|
||||||
|
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
|
||||||
flex: 0 0 29px;
|
flex: 0 0 29px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-language {
|
/* Match the account row's 34px cross-size so the 29px toggle shares the 25px
|
||||||
|
* midline instead of sitting 2.5px above it. */
|
||||||
|
.navbar-top .navbar-form.flex.ac {
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||||
* the absolute panel's static position beside the compact CTA. */
|
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||||
position: relative;
|
* would move that static position to the flex container's cross-start. */
|
||||||
align-self: stretch;
|
.navbar-top .navbar-but > .hover-show {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
width: max-content;
|
|
||||||
max-width: calc(100vw - 24px);
|
|
||||||
margin: 0;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||||
@@ -165,85 +137,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox {
|
.aksrt-header-userbox {
|
||||||
width: 260px;
|
|
||||||
color: var(--color-ink-2);
|
color: var(--color-ink-2);
|
||||||
|
line-height: 1.42857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info {
|
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||||
min-height: 44px;
|
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||||
padding: 2px;
|
.aksrt-header-userbox .muted-box.padding-6 {
|
||||||
|
background-color: var(--this-bg, var(--color-line-soft));
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .avatar {
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
flex: 0 0 44px;
|
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||||
width: 44px;
|
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||||
height: 44px;
|
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||||
object-fit: cover;
|
* declarations are the last word in the cascade. Re-assert them here, which
|
||||||
|
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||||
|
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||||
|
.payvip-icon {
|
||||||
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
|
color: #ece0e0;
|
||||||
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right,
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
.aksrt-header-userbox .user-right > .flex1 {
|
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||||
min-width: 0;
|
display: block;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
.aksrt-header-userbox .user-right b {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .display-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right .img-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .abs-right {
|
|
||||||
top: 50%;
|
|
||||||
right: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .aksrt-notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
right: -4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||||
.aksrt-nav-notification .aksrt-notification-badge {
|
.aksrt-nav-notification .aksrt-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@@ -263,220 +191,18 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-wallet {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet:hover {
|
|
||||||
color: var(--color-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item i {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item b {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: #fff6db !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: #b78103 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin:hover {
|
|
||||||
background: #fff0c2 !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
|
||||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
|
||||||
.aksrt-header-checkin:disabled,
|
|
||||||
.aksrt-header-checkin:disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
opacity: .92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 7px 10px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__exp {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
box-shadow: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
opacity: .9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||||
* icon tiles and must opt out or their labels overlap the following row. */
|
* icon tiles and must opt out or their labels overlap the following row.
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
* The tile geometry itself stays with the shared legacy rules
|
||||||
align-items: flex-start;
|
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||||
gap: 4px;
|
* so the action row keeps the reference proportions. */
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||||
.aksrt-header-userbox .header-user-href > a {
|
.aksrt-header-userbox .header-user-href > a {
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 10px 2px 2px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex: 0 0 36px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-navbar .aksrt-header-userbox {
|
.mobile-navbar .aksrt-header-userbox {
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin-right: auto;
|
margin: auto;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin {
|
|
||||||
background: rgba(255, 193, 7, .18) !important;
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin > i {
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet {
|
|
||||||
background: rgba(255, 255, 255, .06);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet__item,
|
|
||||||
body.dark-theme .aksrt-header-wallet__item b {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.aksrt-header-userbox .header-user-href {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex-basis: 34px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -552,14 +552,15 @@ body.aksrt-modal-runtime-open {
|
|||||||
color: var(--color-ink, #111);
|
color: var(--color-ink, #111);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
|
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .add,
|
||||||
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -575,14 +576,14 @@ body.aksrt-modal-runtime-open {
|
|||||||
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
|
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
|
||||||
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
|
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .preview .preview-item .remove {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
@@ -597,7 +598,7 @@ body.aksrt-modal-runtime-open {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
|
:is(.order-comment-from, .aksrt-report-modal, .aksrt-after-sale-apply-modal) .form-upload .hide {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,3 +613,313 @@ body.aksrt-modal-runtime-open {
|
|||||||
width: calc(100vw - 20px);
|
width: calc(100vw - 20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 售后弹窗(申请 / 售后详细 / 超时效提示,参考主题 modal-mini 几何 1:1)----
|
||||||
|
三个弹窗与禁封申诉弹窗同几何:data_class='modal-mini'(宽 400px),圆角与
|
||||||
|
底色走 --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-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .aksrt-modal-runtime__skeleton {
|
||||||
|
padding: 24px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 蓝色炫彩标题区(参考主题 zib_get_modal_colorful_header('jb-blue', ...)):120px
|
||||||
|
绝对定位横幅 + 旋转彩斑,后续内容整体下移 120px,与禁封记录弹窗同构。 */
|
||||||
|
.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-withdraw-modal .colorful-bg,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--this-color);
|
||||||
|
background: var(--this-bg);
|
||||||
|
background-size: 120%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 + * {
|
||||||
|
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-withdraw-modal .modal-colorful-header > .close,
|
||||||
|
.aksrt-order-detail-modal .modal-colorful-header > .close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 9;
|
||||||
|
color: var(--this-color);
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-after-sale-apply-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-after-sale-detail-modal .colorful-bg > .colorful-make,
|
||||||
|
.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-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);
|
||||||
|
border-radius: 100%;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .colorful-bg > .colorful-make,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make {
|
||||||
|
top: 60%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 323px;
|
||||||
|
height: 290px;
|
||||||
|
animation: aksrt-ban-colorful 120s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .colorful-bg > .colorful-make::before,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::before {
|
||||||
|
top: -325px;
|
||||||
|
right: -558px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 677px;
|
||||||
|
height: 300px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: rotate(341deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-withdraw-modal .colorful-bg > .colorful-make::after,
|
||||||
|
.aksrt-order-detail-modal .colorful-bg > .colorful-make::after {
|
||||||
|
top: -170px;
|
||||||
|
bottom: 0;
|
||||||
|
left: -262px;
|
||||||
|
width: 402px;
|
||||||
|
height: 214px;
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: rotate(326deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 售后原因徽章:与举报弹窗一致,可换行且间距走 mr6/mb6 工具类。 */
|
||||||
|
.aksrt-after-sale-apply-modal .form-but-radio .badg {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单摘要与售后明细行 */
|
||||||
|
.aksrt-after-sale-summary__row,
|
||||||
|
.aksrt-after-sale-detail__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-summary__row:last-child,
|
||||||
|
.aksrt-after-sale-detail__row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-summary__value,
|
||||||
|
.aksrt-after-sale-detail__value {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-detail__statusRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-reason-text {
|
||||||
|
color: var(--color-ink-2, #4e5358);
|
||||||
|
background: var(--surface-2, #eee);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-images img {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--color-line, #e5e7eb);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-after-sale-status.is-pending { color: var(--color-warning, #f59e0b); }
|
||||||
|
.aksrt-after-sale-status.is-processing { color: var(--color-info, #3b82f6); }
|
||||||
|
.aksrt-after-sale-status.is-approved,
|
||||||
|
.aksrt-after-sale-status.is-completed { color: var(--color-success, #10b981); }
|
||||||
|
.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; }
|
||||||
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 社交登录分隔线 */
|
|
||||||
.social-login-divider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 24px 0 16px;
|
|
||||||
color: var(--color-ink-3, #94a3b8);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.social-login-divider::before,
|
|
||||||
.social-login-divider::after {
|
|
||||||
content: '';
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--color-line, #e2e8f0);
|
|
||||||
}
|
|
||||||
.social-login-divider span {
|
|
||||||
padding: 0 16px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-btn {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: var(--radius-btn);
|
|
||||||
border: 1px solid var(--color-line);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
transition: border-color 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
.oauth-btn i {
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-btn:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-footer-link {
|
.auth-footer-link {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|||||||
@@ -50,8 +50,9 @@
|
|||||||
color: var(--color-primary, #f04494);
|
color: var(--color-primary, #f04494);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||||
.widget a.but {
|
.widget a.but,
|
||||||
|
.widget a.img-badge {
|
||||||
color: var(--this-color, #fff);
|
color: var(--this-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,27 @@
|
|||||||
z-index: 100055;
|
z-index: 100055;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* public.css redeclares .muted-box after legacy utilities, overriding its
|
||||||
|
background, radius, and padding utilities. Restore the reference box contract. */
|
||||||
|
.modal[data-aksrt-vip-points-modal] .muted-box.padding-h10 {
|
||||||
|
background-color: var(--this-bg, var(--muted-border-color));
|
||||||
|
border-radius: var(--main-radius);
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal[data-aksrt-vip-points-modal] .muted-box.padding-10,
|
||||||
|
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||||
|
background-color: var(--this-bg, var(--muted-border-color));
|
||||||
|
border-radius: var(--main-radius);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: var(--muted-2-color, #999);
|
||||||
|
}
|
||||||
|
|
||||||
.pay-vip-form-row {
|
.pay-vip-form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -102,21 +123,85 @@
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v2 {
|
/* 会员弹窗骨架屏(pay-vip.js vipSkeleton()):仿真实弹窗布局——
|
||||||
|
用户框 + 等级卡行 + 双列套餐面板。线条/色块与 shimmer 动画复用全局
|
||||||
|
.aksrt-modal-runtime__skeleton-line/-block 与 @keyframes
|
||||||
|
aksrt-modal-skeleton(build/public.min.css 已全局入队);
|
||||||
|
这里只定义 VIP 专属形状(头像圆、tab 卡、双列 grid、按钮条),
|
||||||
|
与 .pay-vip-form-row 的 640px 双列阈值保持一致。 */
|
||||||
|
.aksrt-vip-modal-skeleton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 10px;
|
||||||
gap: 4px;
|
}
|
||||||
padding: 8px 10px;
|
|
||||||
|
.aksrt-vip-modal-skeleton__avatar {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__lines {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__tab {
|
||||||
|
flex: 1;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__pane {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__products {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aksrt-vip-modal-skeleton__button {
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 专属形状共用骨架底色与 shimmer(值与 modal-runtime.css 的线条/块一致)。 */
|
||||||
|
.aksrt-vip-modal-skeleton__avatar,
|
||||||
|
.aksrt-vip-modal-skeleton__tab,
|
||||||
|
.aksrt-vip-modal-skeleton__button {
|
||||||
|
display: block;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #fff;
|
background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16));
|
||||||
background: linear-gradient(90deg, #ffb347, #ff6b6b);
|
background-size: 200% 100%;
|
||||||
|
animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v2.level-2 {
|
@media (min-width: 640px) {
|
||||||
background: linear-gradient(90deg, #9b59b6, #3f51b5);
|
.aksrt-vip-modal-skeleton__pane {
|
||||||
}
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
}
|
||||||
.vip-expdate-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
+16
-14
@@ -2676,6 +2676,13 @@ select.form-control {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .form-select::before 已经画了 FA5 箭头,隐藏原生箭头避免出现两个。 */
|
||||||
|
.form-select select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
.form-select::before {
|
.form-select::before {
|
||||||
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
||||||
content: '\f107';
|
content: '\f107';
|
||||||
@@ -5929,7 +5936,7 @@ body:has(.modal.in) .float-btn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.payvip-icon:before {
|
.payvip-icon:before {
|
||||||
content: '\f219';
|
content: '\f3a5';
|
||||||
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
||||||
font-size: 3.5em;
|
font-size: 3.5em;
|
||||||
right: -0.2em !important;
|
right: -0.2em !important;
|
||||||
@@ -5937,16 +5944,6 @@ body:has(.modal.in) .float-btn,
|
|||||||
opacity: 0.06;
|
opacity: 0.06;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v1 {
|
|
||||||
background: linear-gradient(60deg, #f0bf81 0, #fdefd6 30%, #f2deb9 40%, #fdc277 80%, #f9e1b7 100%);
|
|
||||||
color: #8c5728;
|
|
||||||
--this-color: #8c5728;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vipbg-v1:before {
|
|
||||||
content: '\f006';
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-texticon {
|
.vip-texticon {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -9658,11 +9655,16 @@ img.alignleft {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap
|
/* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
|
||||||
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高;
|
* 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
|
||||||
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */
|
* "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
|
||||||
|
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
|
||||||
|
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
|
||||||
.user-card .card-content .img-badge {
|
.user-card .card-content .img-badge {
|
||||||
line-height: 1.42857143;
|
line-height: 1.42857143;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: auto;
|
||||||
|
text-rendering: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card name .display-name {
|
.user-card name .display-name {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
|
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
.friend-card__avatar {
|
.friend-card__avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-avatar, 50%);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
|
|||||||
@@ -47,10 +47,10 @@
|
|||||||
.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 { 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 > 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 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, .aksrt-order-detail__actions .btn-primary, .aksrt-order-detail__actions .btn-outline, .aksrt-order-detail__actions .btn-ghost { 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__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, .aksrt-order-detail__actions .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); }
|
.aksrt-orders-page .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); }
|
||||||
.aksrt-orders-page .btn-outline, .aksrt-order-detail__actions .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); }
|
.aksrt-orders-page .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); }
|
||||||
.aksrt-orders-page .btn-ghost, .aksrt-order-detail__actions .btn-ghost { background: transparent; color: var(--orders-muted); }
|
.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-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 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); }
|
||||||
@@ -108,9 +108,16 @@
|
|||||||
--detail-success: var(--modal-success);
|
--detail-success: var(--modal-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { width: min(480px, calc(100vw - 32px)); }
|
/* 订单详情弹窗:与售后弹窗统一为 modal-mini 几何(400px);标题区改用蓝色炫彩头,运行时不再渲染默认标题栏。 */
|
||||||
.aksrt-order-detail-modal .aksrt-modal-runtime__header { padding: 15px 18px 14px; border-bottom: 1px solid var(--detail-line); }
|
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||||
.aksrt-order-detail-modal .aksrt-modal-runtime__title { font-size: 16px; font-weight: 700; }
|
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-modal .aksrt-modal-runtime__content { display: flex; flex-direction: column; gap: 18px; padding: 16px 18px 0; }
|
||||||
|
|
||||||
.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; }
|
.aksrt-order-detail__skeleton { display: grid; gap: 12px; padding: 4px 0; }
|
||||||
@@ -156,8 +163,22 @@
|
|||||||
.aksrt-order-detail__refresh-state p { margin: 0; }
|
.aksrt-order-detail__refresh-state p { margin: 0; }
|
||||||
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
|
.aksrt-order-detail__refresh-state button { grid-column: 2; justify-self: start; }
|
||||||
|
|
||||||
.aksrt-order-detail-modal .aksrt-order-detail__actions { position: sticky; bottom: 0; z-index: 2; flex-direction: row; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin: 0 -18px; padding: 13px 18px; border-top: 1px solid var(--detail-line); background: var(--detail-surface); }
|
/* 底部操作区:按钮排布沿用主题 .but-average.modal-buts,此处仅保留吸底定位与通栏背景。
|
||||||
.aksrt-order-detail-modal .aksrt-order-detail__actions > button { flex: 0 0 auto; min-height: 38px; padding: 0 16px; white-space: nowrap; }
|
主题 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 */
|
/* Orders extensions */
|
||||||
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||||
@@ -176,5 +197,19 @@
|
|||||||
.aksrt-coupon-card__meta { margin-bottom: 4px; color: var(--muted-2-color, #888); font-size: 12px; }
|
.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__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; }
|
.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%; }.aksrt-order-detail-modal .aksrt-order-detail__actions > button { flex: 1 1 0; min-width: 0; padding: 0 10px; } }
|
@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); }
|
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;
|
flex-shrink: 0;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
|
/* 用户中心头图卡保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||||
|
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -478,18 +478,6 @@
|
|||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
background: var(--color-primary-softer);
|
background: var(--color-primary-softer);
|
||||||
}
|
}
|
||||||
.binding-item__icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.binding-item__icon i {
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.binding-status {
|
.binding-status {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -190,6 +190,25 @@
|
|||||||
display: flex;
|
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 {
|
.balance-charge-form .zib-widget {
|
||||||
clear: both;
|
clear: both;
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
@@ -304,12 +323,24 @@
|
|||||||
body.uc-modal-open {
|
body.uc-modal-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.btn-block {
|
/* Scoped to the user-center content shell. These are the credits panel's own
|
||||||
display: block;
|
* button skins, and every element they target renders inside .uc-content (the
|
||||||
width: 100%;
|
* panels and their modals are all emitted through templates/parts/user-center/
|
||||||
}
|
* panel.php, which templates/pages/page-profile.php wraps in .uc-content).
|
||||||
.but.jb-blue,
|
*
|
||||||
.but.b-blue-2 {
|
* Left unscoped they are (0,2,0) rules that load after public.css, so they
|
||||||
|
* out-rank the legacy .but.radius pill and flatten every .but.jb-blue on the
|
||||||
|
* page down to 4px. The top-nav publish chip is .but.jb-blue.radius, so on the
|
||||||
|
* user-center route it lost its 50px pill and no longer matched the homepage.
|
||||||
|
* Site-wide .but.jb-blue callers outside the panel (login, submit, badge and
|
||||||
|
* cashier buttons) also picked up the flat #3b82f6 skin instead of the legacy
|
||||||
|
* gradient.
|
||||||
|
*
|
||||||
|
* The .btn-block restatement that used to sit here was byte-identical to the
|
||||||
|
* one in core/buttons.css, which loads earlier on every page, so it could never
|
||||||
|
* win and is dropped rather than scoped. */
|
||||||
|
.uc-content .but.jb-blue,
|
||||||
|
.uc-content .but.b-blue-2 {
|
||||||
background: #3b82f6;
|
background: #3b82f6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -317,18 +348,18 @@ body.uc-modal-open {
|
|||||||
padding: 0.45em 1em;
|
padding: 0.45em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.but.c-blue-2.hollow,
|
.uc-content .but.c-blue-2.hollow,
|
||||||
.but.c-green.hollow {
|
.uc-content .but.c-green.hollow {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.4em 0.9em;
|
padding: 0.4em 0.9em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.but.c-blue-2 {
|
.uc-content .but.c-blue-2 {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
.but.c-green {
|
.uc-content .but.c-green {
|
||||||
color: #16a34a;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,13 +110,6 @@
|
|||||||
gap: 4px 16px;
|
gap: 4px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 待偿欠款:退款收回不足挂账,副信息里以描边标签弱提示。 */
|
|
||||||
.uc-content .uc-commission-card__debt {
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, .22);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 立即提现按钮:贴右居中,对齐参考主题 .abs-right。 */
|
/* 立即提现按钮:贴右居中,对齐参考主题 .abs-right。 */
|
||||||
.uc-content .uc-commission-card__action {
|
.uc-content .uc-commission-card__action {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -214,9 +207,3 @@
|
|||||||
.uc-content .uc-revenue-summary-row--explain { grid-template-columns: 1fr; }
|
.uc-content .uc-revenue-summary-row--explain { grid-template-columns: 1fr; }
|
||||||
.uc-content .uc-revenue-summary-row--explain .uc-reward-richtext { grid-column: 1; }
|
.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-settings__section--social .binding-captcha { margin-bottom: 12px; }
|
||||||
.aksrt-account-list--social .binding-item { border: 0; }
|
.aksrt-account-list--social .binding-item { border: 0; }
|
||||||
.aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; }
|
.aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; }
|
||||||
.aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); }
|
|
||||||
.aksrt-account-list--social .binding-item__icon i { font-size: 22px; line-height: 1; }
|
|
||||||
.aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; }
|
.aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; }
|
||||||
.aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
|
.aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
|
||||||
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
|
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
|
||||||
|
|||||||
@@ -622,25 +622,6 @@ body.aksrt-user-center .uc-content,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uc-service-grid__icon small {
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -7px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 99px;
|
|
||||||
background: var(--color-error, #ef4444);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 9px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uc-sidebar__pair {
|
.uc-sidebar__pair {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -684,7 +665,8 @@ body.aksrt-user-center .uc-content,
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uc-summary-card__body > i {
|
.uc-summary-card__body > i,
|
||||||
|
.uc-summary-card__body > svg {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,14 +698,26 @@ body.aksrt-user-center .uc-content,
|
|||||||
|
|
||||||
.uc-center-card strong { font-size: 13px; }
|
.uc-center-card strong { font-size: 13px; }
|
||||||
.uc-center-card small { margin-top: 3px; color: var(--color-ink-4); font-size: 10px; }
|
.uc-center-card small { margin-top: 3px; color: var(--color-ink-4); font-size: 10px; }
|
||||||
.uc-center-card > i { color: var(--color-ink-4); font-size: 11px; }
|
.uc-center-card > i,
|
||||||
|
.uc-center-card > svg { color: var(--color-ink-4); font-size: 11px; }
|
||||||
.uc-center-card--creation .uc-center-card__icon { color: #6650c7; background: rgba(124, 92, 220, 0.12); }
|
.uc-center-card--creation .uc-center-card__icon { color: #6650c7; background: rgba(124, 92, 220, 0.12); }
|
||||||
.uc-center-card--revenue .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); }
|
.uc-center-card--revenue .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); }
|
||||||
|
|
||||||
.uc-service-grid {
|
/* Services / settings grids, ported from the reference theme sidebar:
|
||||||
display: grid;
|
four per row, no tinted tile behind the icon. */
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
.user-icon-but-box > a {
|
||||||
gap: 8px;
|
width: calc(25% - 10px);
|
||||||
|
margin: 5px;
|
||||||
|
min-width: 50px;
|
||||||
|
max-width: 100px;
|
||||||
|
color: var(--color-ink-3);
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon-but-box > a:hover,
|
||||||
|
.user-icon-but-box > a.is-active {
|
||||||
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.uc-follow-management__list { display: grid; gap: 10px; margin-top: 16px; }
|
.uc-follow-management__list { display: grid; gap: 10px; margin-top: 16px; }
|
||||||
@@ -735,44 +729,3 @@ body.aksrt-user-center .uc-content,
|
|||||||
.uc-follow-management__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
|
.uc-follow-management__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
|
||||||
@media (max-width: 560px) { .uc-follow-management__item { align-items: flex-start; flex-wrap: wrap; } .uc-follow-management__actions { width: 100%; margin-left: 60px; } }
|
@media (max-width: 560px) { .uc-follow-management__item { align-items: flex-start; flex-wrap: wrap; } .uc-follow-management__actions { width: 100%; margin-left: 60px; } }
|
||||||
|
|
||||||
.uc-service-grid__item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 4px 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uc-service-grid__icon {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uc-service-grid__item--cart .uc-service-grid__icon { color: #dd6941; background: rgba(238, 109, 61, 0.12); }
|
|
||||||
.uc-service-grid__item--level .uc-service-grid__icon { color: #9b6b0a; background: rgba(234, 179, 8, 0.13); }
|
|
||||||
.uc-service-grid__item--revenue .uc-service-grid__icon { color: #b8377d; background: rgba(219, 39, 119, 0.12); }
|
|
||||||
.uc-service-grid__item--auth .uc-service-grid__icon { color: #287bc4; background: rgba(14, 165, 233, 0.12); }
|
|
||||||
.uc-service-grid__item--orders .uc-service-grid__icon { color: #c05621; background: rgba(234, 88, 12, 0.12); }
|
|
||||||
.uc-service-grid__item--notice .uc-service-grid__icon { color: #d04b65; background: rgba(244, 63, 94, 0.11); }
|
|
||||||
.uc-service-grid__item--profile .uc-service-grid__icon { color: #6550c7; background: rgba(124, 92, 220, 0.12); }
|
|
||||||
.uc-service-grid__item--security .uc-service-grid__icon { color: #16816c; background: rgba(16, 185, 129, 0.12); }
|
|
||||||
|
|
||||||
.uc-service-grid__item:hover,
|
|
||||||
.uc-service-grid__item.is-active {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uc-service-grid__item.is-active .uc-service-grid__icon {
|
|
||||||
box-shadow: 0 0 0 2px var(--color-primary-soft), 0 5px 14px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
@@ -88,6 +88,8 @@
|
|||||||
.aksrt-author-hero__avatar img {
|
.aksrt-author-hero__avatar img {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
|
/* 公开主页 hero 头像保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||||
|
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 4px solid var(--surface-1);
|
border: 4px solid var(--surface-1);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Post list quick-edit dependency toggling.
|
||||||
|
*
|
||||||
|
* The pay quick-edit box hides fields whose dependency rules no longer match
|
||||||
|
* the selected 付费模式 / 支付类型, mirroring the reference theme's field
|
||||||
|
* visibility. Runs on the WP inline-edit row when it is opened or changed.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function readControl(row, fieldId) {
|
||||||
|
var input = row.querySelector(
|
||||||
|
'[name="aksrt_bulk_edit[pay][' + fieldId + ']"],' +
|
||||||
|
' [name="aksrt_bulk_edit[pay][' + fieldId + '][operation]"]'
|
||||||
|
);
|
||||||
|
if (!input) return null;
|
||||||
|
if (input.tagName === 'SELECT') return input.value;
|
||||||
|
var checked = row.querySelector(
|
||||||
|
'[name="aksrt_bulk_edit[pay][' + fieldId + ']"]:checked'
|
||||||
|
);
|
||||||
|
if (input.type === 'radio' && checked) return checked.value;
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(actual, operator, expected) {
|
||||||
|
if (actual === null) return true;
|
||||||
|
if (operator === '==') return actual === expected;
|
||||||
|
if (operator === '!=') return actual !== expected;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRow(row) {
|
||||||
|
var boxes = row.querySelectorAll('.aksrt-bulk-edit-box[data-dependency]');
|
||||||
|
Array.prototype.forEach.call(boxes, function (box) {
|
||||||
|
var rules;
|
||||||
|
try {
|
||||||
|
rules = JSON.parse(box.getAttribute('data-dependency'));
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(rules)) return;
|
||||||
|
|
||||||
|
var visible = rules.every(function (rule) {
|
||||||
|
return compare(readControl(row, rule[0]), rule[1], String(rule[2]));
|
||||||
|
});
|
||||||
|
box.style.display = visible ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRows() {
|
||||||
|
var rows = document.querySelectorAll('.inline-edit-row, #bulk-edit-row');
|
||||||
|
Array.prototype.forEach.call(rows, function (row) {
|
||||||
|
if (row.querySelector('[data-dependency]')) updateRow(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('change', function (event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.name || target.name.indexOf('aksrt_bulk_edit[pay]') !== 0) return;
|
||||||
|
var row = target.closest('.inline-edit-row, #bulk-edit-row');
|
||||||
|
if (row) updateRow(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// WP swaps the inline-edit row without a change event.
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.closest) return;
|
||||||
|
if (target.closest('.editinline, .bulk-action')) window.setTimeout(syncRows, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
syncRows();
|
||||||
|
})();
|
||||||
@@ -67,6 +67,18 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function formatCount(count) {
|
||||||
|
// 与 PHP aksrt_format_number() 同一套缩写:≥10000 → 1.2W+,≥1000 → 1.5K。
|
||||||
|
var safeCount = Math.max(0, parseInt(count, 10) || 0);
|
||||||
|
if (safeCount >= 10000) {
|
||||||
|
return Math.round((safeCount / 10000) * 10) / 10 + 'W+';
|
||||||
|
}
|
||||||
|
if (safeCount >= 1000) {
|
||||||
|
return Math.round((safeCount / 1000) * 10) / 10 + 'K';
|
||||||
|
}
|
||||||
|
return String(safeCount);
|
||||||
|
}
|
||||||
|
|
||||||
function handleLike(btn, commentId) {
|
function handleLike(btn, commentId) {
|
||||||
if (btn.dataset.busy === '1') return;
|
if (btn.dataset.busy === '1') return;
|
||||||
btn.dataset.busy = '1';
|
btn.dataset.busy = '1';
|
||||||
@@ -81,7 +93,7 @@
|
|||||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||||
btn.classList.toggle('actived', active);
|
btn.classList.toggle('actived', active);
|
||||||
var countEl = btn.querySelector('count');
|
var countEl = btn.querySelector('count');
|
||||||
if (countEl) countEl.textContent = String(count);
|
if (countEl) countEl.textContent = formatCount(count);
|
||||||
})
|
})
|
||||||
.catch(function () { notyf('网络错误', 'danger'); })
|
.catch(function () { notyf('网络错误', 'danger'); })
|
||||||
.finally(function () { btn.dataset.busy = ''; });
|
.finally(function () { btn.dataset.busy = ''; });
|
||||||
|
|||||||
@@ -149,15 +149,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatStatCount(count) {
|
||||||
|
// 与 PHP aksrt_format_number() 同一套缩写:≥10000 → 1.2W+,≥1000 → 1.5K。
|
||||||
|
var safeCount = Math.max(0, count);
|
||||||
|
if (safeCount >= 10000) {
|
||||||
|
return Math.round((safeCount / 10000) * 10) / 10 + "W+";
|
||||||
|
}
|
||||||
|
if (safeCount >= 1000) {
|
||||||
|
return Math.round((safeCount / 1000) * 10) / 10 + "K";
|
||||||
|
}
|
||||||
|
return String(safeCount);
|
||||||
|
}
|
||||||
|
|
||||||
function getBaseStatCount(statName) {
|
function getBaseStatCount(statName) {
|
||||||
var statEl = document.querySelector('[data-stat-count="' + statName + '"][data-base-count]');
|
var statEl = document.querySelector('[data-stat-count="' + statName + '"][data-base-count]');
|
||||||
return statEl ? (parseInt(statEl.dataset.baseCount, 10) || 0) : 0;
|
return statEl ? (parseInt(statEl.dataset.baseCount, 10) || 0) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatCounts(statName, count) {
|
function updateStatCounts(statName, count) {
|
||||||
var safeCount = Math.max(0, count);
|
var label = formatStatCount(count);
|
||||||
document.querySelectorAll('[data-stat-count="' + statName + '"]').forEach(function (statEl) {
|
document.querySelectorAll('[data-stat-count="' + statName + '"]').forEach(function (statEl) {
|
||||||
statEl.textContent = String(safeCount);
|
statEl.textContent = label;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+216
-98
@@ -38,6 +38,14 @@
|
|||||||
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCutCount(value) {
|
||||||
|
var number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return String(value);
|
||||||
|
if (number > 999999) return String(Math.round(number / 10000)) + 'W+';
|
||||||
|
if (number > 9999) return String(Math.round(number / 1000) / 10) + 'W+';
|
||||||
|
return String(number);
|
||||||
|
}
|
||||||
|
|
||||||
function api(path, options) {
|
function api(path, options) {
|
||||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||||
return {
|
return {
|
||||||
@@ -64,16 +72,18 @@
|
|||||||
var walletNumber = Number(wallet);
|
var walletNumber = Number(wallet);
|
||||||
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
||||||
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
||||||
element.textContent = walletText;
|
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText;
|
||||||
element.title = walletText;
|
element.textContent = text;
|
||||||
|
element.title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (points !== undefined && points !== null && points !== '') {
|
if (points !== undefined && points !== null && points !== '') {
|
||||||
var pointsNumber = Number(points);
|
var pointsNumber = Number(points);
|
||||||
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
||||||
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
||||||
element.textContent = pointsText;
|
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(points) : pointsText;
|
||||||
element.title = pointsText;
|
element.textContent = text;
|
||||||
|
element.title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,7 @@
|
|||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
var j = r.json || {};
|
var j = r.json || {};
|
||||||
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
|
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
|
||||||
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title);
|
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
|
||||||
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
|
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
|
||||||
if (copy) copy.addEventListener('click', function () {
|
if (copy) copy.addEventListener('click', function () {
|
||||||
if (!input || !input.value) return;
|
if (!input || !input.value) return;
|
||||||
@@ -140,19 +150,63 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Referral poster (canvas preview modal: hero image + copy + QR + link) =====
|
// ===== Referral poster =====
|
||||||
function initAffiliatePoster(root, url, poster, siteName) {
|
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
|
||||||
|
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
|
||||||
|
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
|
||||||
|
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
|
||||||
|
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
|
||||||
var button = root.querySelector('[data-affiliate-poster]');
|
var button = root.querySelector('[data-affiliate-poster]');
|
||||||
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
|
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
|
||||||
var stage = document.querySelector('[data-affiliate-poster-stage]');
|
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
|
||||||
var download = document.querySelector('[data-affiliate-poster-download]');
|
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 (!button || !modal) return;
|
||||||
if (!url || !poster || poster.enabled !== true) return;
|
if (!url || !poster || poster.enabled !== true) return;
|
||||||
button.hidden = false;
|
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;
|
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) {
|
if (download) {
|
||||||
download.addEventListener('click', function () {
|
download.addEventListener('click', function () {
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
@@ -168,20 +222,47 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
button.addEventListener('click', function () {
|
button.addEventListener('click', function () {
|
||||||
if (typeof window.qrcode !== 'function') {
|
if (typeof window.qrcode !== 'function') {
|
||||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
|
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
|
||||||
openModal(modal);
|
openPosterModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '正在生成海报…' + '</p>'; }
|
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
|
||||||
if (download) download.hidden = true;
|
if (loading) loading.style.display = '';
|
||||||
openModal(modal);
|
if (download) download.classList.add('hide');
|
||||||
|
openPosterModal();
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
buildPosterCanvas(url, poster, siteName).then(function (built) {
|
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
|
||||||
canvas = built;
|
canvas = built;
|
||||||
if (stage) { stage.innerHTML = ''; stage.appendChild(built); }
|
if (!stage) return;
|
||||||
if (download) download.hidden = false;
|
// 参考主题: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 () {
|
}).catch(function () {
|
||||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
|
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
|
||||||
}).finally(function () {
|
}).finally(function () {
|
||||||
@@ -190,89 +271,141 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参考主题 poster-share.js 布局 1:1:768×1168,顶部主图占上半屏,
|
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
|
||||||
// 左上角日期,标题竖蓝线 + 标题,正文,底部带边框盒(logo/站名 + 二维码 + 链接)。
|
function loadPosterImage(sources) {
|
||||||
function buildPosterCanvas(url, poster, siteName) {
|
var list = (sources || []).filter(function (src) { return !!src; });
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var index = 0;
|
||||||
|
function next() {
|
||||||
|
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
|
||||||
|
var image = new Image();
|
||||||
|
image.crossOrigin = 'anonymous';
|
||||||
|
image.onload = function () { resolve(image); };
|
||||||
|
image.onerror = function () { index += 1; next(); };
|
||||||
|
image.src = list[index];
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
|
||||||
|
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
|
||||||
|
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
|
||||||
|
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
|
||||||
|
dHeight = (srcH * dWidth) / srcW;
|
||||||
|
dy = sy + (boxH - dHeight) / 2;
|
||||||
|
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
|
||||||
|
dWidth = (srcW * dHeight) / srcH;
|
||||||
|
dx = sx + (boxW - dWidth) / 2;
|
||||||
|
}
|
||||||
|
return { x: dx, y: dy, width: dWidth, height: dHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
|
||||||
|
// 返回实际测量宽度,供价格/标签层做横向排版。
|
||||||
|
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
|
||||||
|
var rows = [];
|
||||||
|
var temp = '';
|
||||||
|
var limit = maxWidth - 120;
|
||||||
|
x = x + 38;
|
||||||
|
text = String(text || '').replace(/[\r\n]/g, '');
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.font = font;
|
||||||
|
var chars = text.split('');
|
||||||
|
var measured = 0;
|
||||||
|
if (ctx.measureText(text).width <= limit) {
|
||||||
|
rows.push(text);
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < chars.length; i++) {
|
||||||
|
if (rows.length === maxRow && i < chars.length - 1) {
|
||||||
|
rows[rows.length - 1] += '...';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ctx.measureText(temp).width < limit) {
|
||||||
|
temp += chars[i];
|
||||||
|
if (i === chars.length - 1) rows.push(temp);
|
||||||
|
} else {
|
||||||
|
i--; // 参考主题:回退一格防止字符丢失
|
||||||
|
rows.push(temp);
|
||||||
|
temp = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var r = 0; r < rows.length; r++) {
|
||||||
|
measured = Math.max(measured, ctx.measureText(rows[r]).width);
|
||||||
|
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
|
||||||
|
}
|
||||||
|
return { width: measured, height: rows.length * lineHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
|
||||||
|
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
|
||||||
|
function buildPosterCanvas(url, poster, siteName, referrerName) {
|
||||||
var width = 768, height = 1168;
|
var width = 768, height = 1168;
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
|
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
|
||||||
|
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
|
||||||
|
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
|
||||||
|
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
|
||||||
|
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
|
||||||
|
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
|
||||||
|
var bottom = String(poster.bottom || '扫描二维码阅读全文');
|
||||||
|
var title = String(poster.title || siteName || '');
|
||||||
|
var description = String(poster.description || '');
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
|
||||||
|
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
|
||||||
|
]).then(function (assets) {
|
||||||
|
var banner = assets[0], logo = assets[1];
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
var loadImage = null;
|
// 顶部主图(cover 裁切)。
|
||||||
if (poster.image) {
|
var rect = coverRect(width, imgHeight, banner.width, banner.height);
|
||||||
loadImage = new Promise(function (resolve) {
|
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
|
||||||
var image = new Image();
|
|
||||||
image.crossOrigin = 'anonymous';
|
|
||||||
image.onload = function () { resolve(image); };
|
|
||||||
image.onerror = function () { resolve(null); };
|
|
||||||
image.src = poster.image;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
loadImage = Promise.resolve(null);
|
|
||||||
}
|
|
||||||
return loadImage.then(function (image) {
|
|
||||||
var imgHeight = height / 2; // 参考主题 rebate(非 product):顶部主图占上半屏。
|
|
||||||
// 顶部主图(cover 裁切)或纯色占位。
|
|
||||||
if (image && image.width && image.height) {
|
|
||||||
var rect = coverRect(width, imgHeight, image.width, image.height);
|
|
||||||
ctx.drawImage(image, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = '#0f172a';
|
|
||||||
ctx.fillRect(0, 0, width, imgHeight);
|
|
||||||
}
|
|
||||||
// 左上角日期(白字,覆盖在主图上)。
|
// 左上角日期(白字,覆盖在主图上)。
|
||||||
var now = new Date();
|
|
||||||
var week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.95)';
|
ctx.fillStyle = 'rgba(255,255,255,1)';
|
||||||
ctx.font = 'bold 35px Arial, sans-serif';
|
ctx.font = '35px Arial';
|
||||||
ctx.fillText(week[now.getDay()], 40, 35);
|
ctx.fillText(weekLabel, 30, 37);
|
||||||
ctx.font = '28px Arial, sans-serif';
|
ctx.font = '28px Arial';
|
||||||
ctx.fillText((now.getMonth() + 1) + ' / ' + now.getDate(), 40, 88);
|
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
|
||||||
// 标题竖蓝线。
|
// 标题竖蓝线。
|
||||||
ctx.strokeStyle = '#00a1ff';
|
ctx.strokeStyle = '#00a1ff';
|
||||||
ctx.lineWidth = 7;
|
ctx.lineWidth = 7;
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
ctx.lineCap = 'round';
|
||||||
ctx.moveTo(40, imgHeight + 38);
|
ctx.moveTo(40, imgHeight + 38);
|
||||||
ctx.lineTo(40, imgHeight + 74);
|
ctx.lineTo(40, imgHeight + 74);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
// 标题(粗体,最多 2 行)。
|
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
|
||||||
ctx.fillStyle = '#424242';
|
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
|
||||||
ctx.font = 'bold 35px Arial, sans-serif';
|
// drawTextOverflow 内部会 +38,所以这里传 2。
|
||||||
var titleLines = wrapText(ctx, String(poster.title || siteName || ''), width - 100).slice(0, 2);
|
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
|
||||||
var cursorY = imgHeight + 40;
|
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
|
||||||
for (var t = 0; t < titleLines.length; t++) {
|
if (tags) {
|
||||||
ctx.fillText(titleLines[t], 60, cursorY);
|
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
|
||||||
cursorY += 52;
|
|
||||||
}
|
}
|
||||||
// 正文(简介,最多 3 行)。
|
// 简介层(灰):$content 置于 (0, img_height+220)。
|
||||||
var description = String(poster.description || '');
|
|
||||||
if (description) {
|
if (description) {
|
||||||
ctx.fillStyle = '#585858';
|
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
|
||||||
ctx.font = '25px Arial, sans-serif';
|
|
||||||
var lines = wrapText(ctx, description, width - 80).slice(0, 3);
|
|
||||||
var contentY = imgHeight + 160;
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
ctx.fillText(lines[i], 40, contentY);
|
|
||||||
contentY += 38;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 底部带边框盒。
|
// 底部带边框盒。
|
||||||
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
|
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeRect(30, height - 230, width - 60, 200);
|
ctx.beginPath();
|
||||||
// 站点名(logo 位,左侧)。
|
ctx.rect(30, height - 230, width - 60, 200);
|
||||||
ctx.fillStyle = '#424242';
|
ctx.stroke();
|
||||||
ctx.font = 'bold 30px Arial, sans-serif';
|
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
|
||||||
ctx.fillText(String(siteName || ''), 80, height - 175);
|
if (logo && logo.width && logo.height) {
|
||||||
ctx.fillStyle = '#94a3b8';
|
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
|
||||||
ctx.font = '20px Arial, sans-serif';
|
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
|
||||||
ctx.fillText('长按识别二维码访问', 80, height - 130);
|
}
|
||||||
// 二维码(右下)。
|
// 二维码(右下角)。
|
||||||
var qr = window.qrcode(0, 'M');
|
var qr = window.qrcode(0, 'M');
|
||||||
qr.addData(url);
|
qr.addData(url);
|
||||||
qr.make();
|
qr.make();
|
||||||
@@ -288,11 +421,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 底部链接(居中)。
|
// 底部文案(居中,盒下方)。
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillStyle = '#b4b4b4';
|
ctx.fillStyle = '#b4b4b4';
|
||||||
ctx.font = '22px Arial, sans-serif';
|
ctx.font = '22px Arial';
|
||||||
ctx.fillText(String(url).slice(0, 46), width / 2, height - 46);
|
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
return canvas;
|
return canvas;
|
||||||
});
|
});
|
||||||
@@ -311,21 +444,6 @@
|
|||||||
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
|
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapText(ctx, text, maxWidth) {
|
|
||||||
var lines = [];
|
|
||||||
var current = '';
|
|
||||||
for (var i = 0; i < text.length; i++) {
|
|
||||||
if (ctx.measureText(current + text[i]).width > maxWidth && current) {
|
|
||||||
lines.push(current);
|
|
||||||
current = text[i];
|
|
||||||
} else {
|
|
||||||
current += text[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current) lines.push(current);
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
|
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
|
||||||
|
|
||||||
// ===== VIP 开通 =====
|
// ===== VIP 开通 =====
|
||||||
@@ -1193,7 +1311,7 @@
|
|||||||
var number = document.createElement('span');
|
var number = document.createElement('span');
|
||||||
number.className = 'uc-withdraw-history__number muted-2-color em09';
|
number.className = 'uc-withdraw-history__number muted-2-color em09';
|
||||||
var numberLabel = document.createElement('span');
|
var numberLabel = document.createElement('span');
|
||||||
numberLabel.textContent = '流水号:';
|
numberLabel.textContent = '订单号:';
|
||||||
number.appendChild(numberLabel);
|
number.appendChild(numberLabel);
|
||||||
var code = document.createElement('code');
|
var code = document.createElement('code');
|
||||||
code.textContent = text(item && item.withdrawal_number);
|
code.textContent = text(item && item.withdrawal_number);
|
||||||
|
|||||||
@@ -103,7 +103,6 @@
|
|||||||
this.bindFeatured();
|
this.bindFeatured();
|
||||||
this.bindPay();
|
this.bindPay();
|
||||||
this.bindLeaveWarning();
|
this.bindLeaveWarning();
|
||||||
this.bindAi();
|
|
||||||
this.bindTagActive();
|
this.bindTagActive();
|
||||||
this.loadFeaturedFromDom();
|
this.loadFeaturedFromDom();
|
||||||
},
|
},
|
||||||
@@ -635,67 +634,6 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
bindAi: function () {
|
|
||||||
var ai = cfg.ai || {};
|
|
||||||
if (!ai.available) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
qsa('.aksrt-ai-assist-btn').forEach(function (btn) {
|
|
||||||
btn.hidden = false;
|
|
||||||
btn.addEventListener('click', function () {
|
|
||||||
var action = btn.getAttribute('data-ai-action');
|
|
||||||
var content = getEditorContent();
|
|
||||||
if (!content || content.replace(/<[^>]+>/g, '').trim().length < 10) {
|
|
||||||
notyf(i18n.needContent || '请先填写正文内容', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var toastId = action === 'tags'
|
|
||||||
? 'aksrt-front-submit-ai-tags-toast'
|
|
||||||
: 'aksrt-front-submit-ai-summary-toast';
|
|
||||||
var old = btn.textContent;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = i18n.aiGenerating || '...';
|
|
||||||
notyf(i18n.aiGenerating || '正在生成…', 'load', '', toastId);
|
|
||||||
fetch((cfg.restUrl || '').replace(/\/$/, '') + '/ai/' + action, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-WP-Nonce': cfg.restNonce || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ content: content }),
|
|
||||||
})
|
|
||||||
.then(function (r) {
|
|
||||||
return r.json();
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
var text = (res && (res.data || res).text) || (res && res.result) || '';
|
|
||||||
if (!text) {
|
|
||||||
throw new Error(i18n.aiFailed || 'AI failed');
|
|
||||||
}
|
|
||||||
if (action === 'summary') {
|
|
||||||
var ex = qs('[name="excerpt"]');
|
|
||||||
if (ex) {
|
|
||||||
ex.value = text;
|
|
||||||
}
|
|
||||||
} else if (action === 'tags') {
|
|
||||||
var tags = qs('[name="tags"]');
|
|
||||||
if (tags) {
|
|
||||||
tags.value = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notyf(i18n.aiFilled || '已填入,可继续编辑', 'success', 5, toastId);
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
notyf(err.message || i18n.aiFailed || 'AI 生成失败', 'danger', 5, toastId);
|
|
||||||
})
|
|
||||||
.finally(function () {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = old;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
||||||
|
|||||||
@@ -0,0 +1,700 @@
|
|||||||
|
/**
|
||||||
|
* AKSRT After-Sale — 前台售后弹窗(申请 / 超时效提示 / 售后详细)
|
||||||
|
*
|
||||||
|
* 与「禁封/举报」链路同构(report.js / user-ban.js):
|
||||||
|
* - [data-aksrt-after-sale-apply] → 申请售后弹窗(超时效则改为超时效提示弹窗)
|
||||||
|
* - [data-aksrt-after-sale-detail] → 售后详细弹窗(可再次申请 / 不可再申请提示)
|
||||||
|
* - [data-aksrt-after-sale-withdraw] → 撤销售后二次确认弹窗(仅待审核工单),确认后 DELETE 工单
|
||||||
|
* 两个入口的 data-* 属性携带订单与售后状态载荷(同 report.js 的 data-aksrt-report),
|
||||||
|
* 点击即同步弹窗,不做前置请求(避免首次点击无反馈)。
|
||||||
|
* - 提交走 REST /aksrt/v1/orders/{order_number}/after-sale
|
||||||
|
* - 撤销走 DELETE 同端点(仅待审核工单,后端置为已取消)
|
||||||
|
* - 提交与撤销前先过人机验证(after_sale_apply / after_sale_cancel,场景未启用自动放行)
|
||||||
|
* - 举证图片走 /orders/{order_number}/after-sale/upload-image(复用评论图片上传校验)
|
||||||
|
*
|
||||||
|
* 按钮形态由订单视图模型的 after_sale 状态决定(UserOrders::after_sale_state):
|
||||||
|
* 已申请 → 售后详细;未申请且在时效内 → 申请售后;超时效 → 申请售后(点击弹超时效提示)。
|
||||||
|
* 弹窗统一经 AksrtModalRuntime.open 直出 HTML:桌面为主题 modal-mini 几何
|
||||||
|
* (400px)+ 蓝色炫彩标题区(modal-colorful-header colorful-bg jb-blue),
|
||||||
|
* 移动端由 mobileBottom 底部抽屉接管。
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var API_ROOT = (function () {
|
||||||
|
var d = window.aksrtData || {};
|
||||||
|
var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1';
|
||||||
|
return String(raw).replace(/\/$/, '');
|
||||||
|
})();
|
||||||
|
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
|
||||||
|
var MAX_IMAGES = 3;
|
||||||
|
|
||||||
|
var STATUS_LABELS = {
|
||||||
|
pending: '待审核',
|
||||||
|
processing: '处理中',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已拒绝',
|
||||||
|
cancelled: '已取消',
|
||||||
|
completed: '已完成',
|
||||||
|
};
|
||||||
|
|
||||||
|
function settings() {
|
||||||
|
var d = window.aksrtData && window.aksrtData.afterSale;
|
||||||
|
return d && typeof d === 'object' ? d : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasonOptions() {
|
||||||
|
var list = settings().reasons;
|
||||||
|
if (!Array.isArray(list)) return [];
|
||||||
|
return list
|
||||||
|
.map(function (item) {
|
||||||
|
return {
|
||||||
|
slug: String((item && (item.slug || item.value)) || ''),
|
||||||
|
name: String((item && (item.name || item.label || item.t)) || ''),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(function (item) { return item.name !== ''; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function materialOptions() {
|
||||||
|
var list = settings().materials;
|
||||||
|
if (!Array.isArray(list)) return [];
|
||||||
|
return list.map(function (item) { return String(item == null ? '' : item); })
|
||||||
|
.filter(function (text) { return text !== ''; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value == null ? '' : value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorfulHeader(title, icon) {
|
||||||
|
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
||||||
|
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
|
||||||
|
'<div class="colorful-make"></div>' +
|
||||||
|
'<div class="text-center"><div class="em2x"><i class="fa fas ' + escapeHtml(icon) + '" aria-hidden="true"></i></div>' +
|
||||||
|
'<div class="mt10 em12 padding-w10">' + escapeHtml(title) + '</div></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderAmountText(order) {
|
||||||
|
var raw = String(order && order.amount != null ? order.amount : '0');
|
||||||
|
if (order && order.is_points) {
|
||||||
|
return escapeHtml(String(order.currency_symbol || '积分')) + ' ' + (/^\d+$/.test(raw) ? raw : '0');
|
||||||
|
}
|
||||||
|
return '¥' + raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderSummaryHtml(order) {
|
||||||
|
if (!order) return '';
|
||||||
|
return '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
|
||||||
|
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">商品</span>' +
|
||||||
|
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.product_name || '订单') + '</span></div>' +
|
||||||
|
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">订单号</span>' +
|
||||||
|
'<span class="aksrt-after-sale-summary__value">' + escapeHtml(order.order_number || '') + '</span></div>' +
|
||||||
|
'<div class="aksrt-after-sale-summary__row"><span class="muted-2-color">实付</span>' +
|
||||||
|
'<span class="aksrt-after-sale-summary__value">' + orderAmountText(order) + '</span></div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApplyHtml(order) {
|
||||||
|
var cfg = settings();
|
||||||
|
var reasons = reasonOptions();
|
||||||
|
var materials = materialOptions();
|
||||||
|
|
||||||
|
var html = '<div class="aksrt-after-sale-apply-modal">';
|
||||||
|
html += colorfulHeader('申请售后', 'fa-undo-alt');
|
||||||
|
html += '<form class="aksrt-after-sale-form" data-aksrt-after-sale-order="' + escapeHtml((order && order.order_number) || '') + '" novalidate>';
|
||||||
|
// 滚动容器与禁封申诉弹窗一致:no-scrollbar 完全隐藏滚动条(含细滚动条)+ 50vh 内层滚动。
|
||||||
|
html += '<div class="scroll-y no-scrollbar max-vh5">';
|
||||||
|
html += orderSummaryHtml(order);
|
||||||
|
|
||||||
|
if (reasons.length) {
|
||||||
|
html += '<div class="mb20">';
|
||||||
|
html += '<div class="em09 muted-2-color mb6">售后原因</div>';
|
||||||
|
html += '<div class="form-radio form-but-radio">';
|
||||||
|
reasons.forEach(function (reason) {
|
||||||
|
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-after-sale-reason="' + escapeHtml(reason.name) + '" data-reason-slug="' + escapeHtml(reason.slug) + '">' + escapeHtml(reason.name) + '</span>';
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
materials.forEach(function (label) {
|
||||||
|
html += '<div class="mb10"><div class="muted-2-color mb6">' + escapeHtml(label) + '</div>' +
|
||||||
|
'<input class="form-control" type="text" maxlength="120" data-aksrt-after-sale-material="' + escapeHtml(label) + '" placeholder="请输入' + escapeHtml(label) + '"></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
var placeholder = String(cfg.reasonPlaceholder || '必填,请您详细填写申请原因');
|
||||||
|
html += '<div class="mb10"><div class="muted-2-color mb6">申请说明</div>' +
|
||||||
|
'<textarea class="form-control" name="reason" rows="3" maxlength="2000" required placeholder="' + escapeHtml(placeholder) + '"></textarea></div>';
|
||||||
|
|
||||||
|
if (cfg.imgEnabled) {
|
||||||
|
html += '<div class="form-upload mb10">' +
|
||||||
|
'<div class="em09 muted-2-color mb6">上传图片</div>' +
|
||||||
|
'<div class="preview"><div class="add" data-aksrt-after-sale-add role="button" tabindex="0" aria-label="上传图片"></div></div>' +
|
||||||
|
'<input class="hide" type="file" accept="image/gif,image/jpeg,image/jpg,image/png,image/webp" data-aksrt-after-sale-file>' +
|
||||||
|
'<div class="muted-2-color em09 mt6">最多上传 ' + MAX_IMAGES + ' 张图片</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 申请售后说明(后台 after_sale_desc,服务端已 wp_kses_post)。
|
||||||
|
var desc = String(cfg.desc || '');
|
||||||
|
if (desc) {
|
||||||
|
html += '<div class="em09 padding-h10 muted-box mb10">' + desc + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="but-average modal-buts">' +
|
||||||
|
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
|
||||||
|
'<button type="button" class="but c-blue" data-aksrt-after-sale-submit><i class="fa fas fa-check" aria-hidden="true"></i>确认提交</button>' +
|
||||||
|
'</div>';
|
||||||
|
html += '</form></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailRow(label, value) {
|
||||||
|
if (value === '' || value == null) return '';
|
||||||
|
return '<div class="aksrt-after-sale-detail__row"><span class="muted-2-color">' + escapeHtml(label) + '</span>' +
|
||||||
|
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理记录节点标题:用户侧提交/撤脱离不开工单状态,单独措辞;
|
||||||
|
// 后台提交直接用目标状态,与后台列表口径一致。
|
||||||
|
function handlingEntryTitle(entry) {
|
||||||
|
var status = String((entry && entry.status) || '');
|
||||||
|
if (String((entry && entry.source) || '') === 'user') {
|
||||||
|
return status === 'cancelled' ? '用户撤销售后' : '用户提交申请';
|
||||||
|
}
|
||||||
|
return STATUS_LABELS[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理记录时间线:左列绳结状态条(节点 + 状态 + 时间),右列处理备注。
|
||||||
|
// 数据源为 latest.handling_log(工单行内 JSON,按时间正序)。
|
||||||
|
function handlingLogHtml(log) {
|
||||||
|
var entries = Array.isArray(log) ? log : [];
|
||||||
|
if (!entries.length) {
|
||||||
|
return '<div class="aksrt-after-sale-log"><div class="aksrt-after-sale-log__empty">暂无处理记录</div></div>';
|
||||||
|
}
|
||||||
|
var html = '<div class="aksrt-after-sale-log"><div class="muted-2-color em09 mb6">处理记录</div>';
|
||||||
|
html += '<ol class="aksrt-after-sale-timeline">';
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
var status = String((entry && entry.status) || '');
|
||||||
|
var note = String((entry && entry.note) || '');
|
||||||
|
html += '<li class="aksrt-after-sale-timeline__item is-' + escapeHtml(status) + '">';
|
||||||
|
html += '<span class="aksrt-after-sale-timeline__knot"></span>';
|
||||||
|
html += '<div class="aksrt-after-sale-timeline__body">';
|
||||||
|
// [节点][处理状态][时间] 同一行,处理备注另起一行
|
||||||
|
html += '<div class="aksrt-after-sale-timeline__head">' +
|
||||||
|
'<span class="aksrt-after-sale-timeline__status">' + escapeHtml(handlingEntryTitle(entry)) + '</span>' +
|
||||||
|
'<time class="aksrt-after-sale-timeline__time">' + escapeHtml(String((entry && entry.created_at) || '')) + '</time>' +
|
||||||
|
'</div>';
|
||||||
|
if (note !== '') {
|
||||||
|
html += '<p class="aksrt-after-sale-timeline__note">' + escapeHtml(note).replace(/\r\n|\r|\n/g, '<br>') + '</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '</li>';
|
||||||
|
});
|
||||||
|
html += '</ol></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDetailHtml(order) {
|
||||||
|
var afterSale = (order && order.after_sale) || {};
|
||||||
|
var latest = afterSale.latest || {};
|
||||||
|
var status = String(latest.status || '');
|
||||||
|
|
||||||
|
var html = '<div class="aksrt-after-sale-detail-modal">';
|
||||||
|
html += colorfulHeader('售后详细', 'fa-clipboard-list');
|
||||||
|
html += '<div class="scroll-y no-scrollbar max-vh5 aksrt-after-sale-detail__body">';
|
||||||
|
html += orderSummaryHtml(order);
|
||||||
|
|
||||||
|
html += '<div class="aksrt-after-sale-detail__statusRow">' +
|
||||||
|
'<span class="muted-2-color em09">售后状态</span>' +
|
||||||
|
'<span class="aksrt-after-sale-detail__statusCell">' +
|
||||||
|
'<span class="badg p2-10 aksrt-after-sale-status is-' + escapeHtml(status) + '">' + escapeHtml(STATUS_LABELS[status] || status) + '</span>' +
|
||||||
|
// 仅「待审核」可撤销(后端 cancel 仅放行 pending):入口随状态徽章渲染。
|
||||||
|
(status === 'pending'
|
||||||
|
? '<button type="button" class="aksrt-after-sale-withdraw" data-aksrt-after-sale-withdraw="' + escapeHtml((order && order.order_number) || '') + '">撤销售后<i class="fa fas fa-angle-right" aria-hidden="true"></i></button>'
|
||||||
|
: '') +
|
||||||
|
'</span></div>';
|
||||||
|
|
||||||
|
html += '<div class="aksrt-after-sale-detail__rows">';
|
||||||
|
html += detailRow('申请时间', String(latest.created_at || ''));
|
||||||
|
html += '</div>';
|
||||||
|
// 处理记录:每次后台提交一条,替代原先单行的「处理备注 / 完成时间」。
|
||||||
|
html += handlingLogHtml(latest.handling_log);
|
||||||
|
|
||||||
|
// 申请表单分行展示:预置原因 / 申请资料 / 申请说明各自独立成行,
|
||||||
|
// 与上方「申请时间」等明细行同格式;老数据无 apply_form 时兜底展示融合串。
|
||||||
|
var applyForm = latest.apply_form && typeof latest.apply_form === 'object' ? latest.apply_form : null;
|
||||||
|
if (applyForm) {
|
||||||
|
html += detailRow('原因', String(applyForm.reason_label || ''));
|
||||||
|
(Array.isArray(applyForm.materials) ? applyForm.materials : []).forEach(function (material) {
|
||||||
|
html += detailRow(String((material && material.label) || ''), String((material && material.value) || ''));
|
||||||
|
});
|
||||||
|
html += detailRow('申请说明', String(applyForm.description || ''));
|
||||||
|
} else if (latest.reason) {
|
||||||
|
html += '<div class="mb10"><div class="muted-2-color em09 mb6">申请原因</div>' +
|
||||||
|
'<div class="em09 aksrt-after-sale-reason-text">' + escapeHtml(latest.reason).replace(/\r\n|\r|\n/g, '<br>') + '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var images = Array.isArray(latest.images) ? latest.images : [];
|
||||||
|
if (images.length) {
|
||||||
|
html += '<div class="mb10"><div class="muted-2-color em09 mb6">举证图片</div><div class="aksrt-after-sale-images">';
|
||||||
|
images.forEach(function (url) {
|
||||||
|
html += '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer"><img src="' + escapeHtml(url) + '" alt=""></a>';
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (afterSale.can_reapply) {
|
||||||
|
// 可再次申请:仅保留单个主按钮(与禁封申诉弹窗单按钮同式)。
|
||||||
|
html += '<div class="but-average modal-buts">' +
|
||||||
|
'<button type="button" class="but c-blue" data-aksrt-after-sale-reapply data-aksrt-after-sale-payload="' + escapeHtml(JSON.stringify(order)) + '"><i class="fa fas fa-undo-alt" aria-hidden="true"></i>再次申请售后</button>' +
|
||||||
|
'</div>';
|
||||||
|
} else {
|
||||||
|
// 不可再申请售后:不再展示阻断文案,仅保留关闭入口。
|
||||||
|
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExpiredHtml() {
|
||||||
|
var text = String(settings().expiredText || '抱歉,您的订单已超过售后申请时效');
|
||||||
|
var html = '<div class="aksrt-after-sale-expired-modal">';
|
||||||
|
html += colorfulHeader('售后提示', 'fa-clock');
|
||||||
|
// 超过时效提示(后台 after_sale_expired_text,服务端已 wp_kses_post,支持 HTML)。
|
||||||
|
html += '<div class="aksrt-after-sale-expired__body em09">' + text + '</div>';
|
||||||
|
html += '<div class="but-average modal-buts"><button type="button" class="but c-blue" data-aksrt-modal-close>我知道了</button></div>';
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销售后二次确认弹窗:待审核工单入口点入,确认后 DELETE 售后工单。
|
||||||
|
function buildWithdrawHtml(orderNumber) {
|
||||||
|
var html = '<div class="aksrt-after-sale-withdraw-modal">';
|
||||||
|
html += colorfulHeader('撤销售后', 'fa-undo-alt');
|
||||||
|
html += '<div class="aksrt-after-sale-withdraw__body em09">撤销后本次售后申请将关闭,订单恢复为可再次申请售后的状态。确定撤销吗?</div>';
|
||||||
|
html += '<div class="but-average modal-buts">' +
|
||||||
|
'<button type="button" class="but" data-aksrt-modal-close>取消</button>' +
|
||||||
|
'<button type="button" class="but c-blue" data-aksrt-after-sale-withdraw-confirm="' + escapeHtml(orderNumber) + '">确认撤销</button>' +
|
||||||
|
'</div>';
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(options) {
|
||||||
|
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
|
||||||
|
notyf('售后组件加载失败,请刷新页面后重试', 'danger');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var shell = window.AksrtModalRuntime.open({
|
||||||
|
html: options.html,
|
||||||
|
className: options.className,
|
||||||
|
kind: options.kind,
|
||||||
|
mobileBottom: true,
|
||||||
|
});
|
||||||
|
bindShell(shell);
|
||||||
|
return shell;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openApply(order) {
|
||||||
|
return openModal({
|
||||||
|
html: buildApplyHtml(order),
|
||||||
|
className: 'aksrt-after-sale-apply-modal',
|
||||||
|
kind: 'after-sale-apply',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(order) {
|
||||||
|
return openModal({
|
||||||
|
html: buildDetailHtml(order),
|
||||||
|
className: 'aksrt-after-sale-detail-modal',
|
||||||
|
kind: 'after-sale-detail',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExpired() {
|
||||||
|
return openModal({
|
||||||
|
html: buildExpiredHtml(),
|
||||||
|
className: 'aksrt-after-sale-expired-modal',
|
||||||
|
kind: 'after-sale-expired',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWithdraw(orderNumber) {
|
||||||
|
if (!orderNumber) return;
|
||||||
|
return openModal({
|
||||||
|
html: buildWithdrawHtml(orderNumber),
|
||||||
|
className: 'aksrt-after-sale-withdraw-modal',
|
||||||
|
kind: 'after-sale-withdraw',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析按钮 data-* 上的售后载荷(与举报链路 data-aksrt-report 同模式):
|
||||||
|
* 入口数据随列表一起渲染,点击即可同步弹窗,不做前置请求。
|
||||||
|
*/
|
||||||
|
function parsePayload(raw) {
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(raw);
|
||||||
|
return data && typeof data === 'object' ? data : null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 申请售后入口:超时效 → 超时效提示;已申请 → 售后详细;否则 → 申请表单。
|
||||||
|
function openApplyFlow(payload) {
|
||||||
|
var afterSale = payload && payload.after_sale ? payload.after_sale : null;
|
||||||
|
if (!payload || !afterSale || !afterSale.enabled) {
|
||||||
|
notyf('售后申请暂不可用', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (afterSale.expired) { openExpired(); return; }
|
||||||
|
if (afterSale.has_application) { openDetail(payload); return; }
|
||||||
|
openApply(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetailFlow(payload) {
|
||||||
|
var afterSale = payload && payload.after_sale ? payload.after_sale : null;
|
||||||
|
if (!payload || !afterSale || !afterSale.enabled || !afterSale.has_application) {
|
||||||
|
notyf('暂无售后记录', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openDetail(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectImages(shell) {
|
||||||
|
return Array.prototype.map.call(
|
||||||
|
shell.querySelectorAll('.preview .preview-item img'),
|
||||||
|
function (img) { return img.getAttribute('src') || ''; }
|
||||||
|
).filter(function (url) { return url !== ''; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadImage(shell, file) {
|
||||||
|
var form = shell.querySelector('[data-aksrt-after-sale-order]');
|
||||||
|
var preview = shell.querySelector('.form-upload .preview');
|
||||||
|
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
|
||||||
|
if (!preview || !orderNumber) return Promise.resolve();
|
||||||
|
if (collectImages(shell).length >= MAX_IMAGES) {
|
||||||
|
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new FormData();
|
||||||
|
body.append('image', file);
|
||||||
|
|
||||||
|
return window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale/upload-image', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-WP-Nonce': NONCE },
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (result) {
|
||||||
|
var url = result && result.success && result.data && result.data.url;
|
||||||
|
if (!url) {
|
||||||
|
notyf((result && result.message) || '图片上传失败', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (collectImages(shell).length >= MAX_IMAGES) {
|
||||||
|
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var item = document.createElement('div');
|
||||||
|
item.className = 'preview-item';
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.alt = '';
|
||||||
|
var remove = document.createElement('button');
|
||||||
|
remove.type = 'button';
|
||||||
|
remove.className = 'remove';
|
||||||
|
remove.setAttribute('data-aksrt-after-sale-image-remove', '');
|
||||||
|
remove.setAttribute('aria-label', '删除图片');
|
||||||
|
remove.innerHTML = '×';
|
||||||
|
remove.addEventListener('click', function () { item.remove(); });
|
||||||
|
item.appendChild(img);
|
||||||
|
item.appendChild(remove);
|
||||||
|
preview.insertBefore(item, preview.querySelector('.add'));
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
notyf('图片上传失败,请重试', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedReasonLabel(shell) {
|
||||||
|
var active = shell.querySelector('[data-aksrt-after-sale-reason].active');
|
||||||
|
return active ? String(active.getAttribute('data-aksrt-after-sale-reason') || '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMaterials(shell) {
|
||||||
|
var materials = [];
|
||||||
|
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) {
|
||||||
|
var value = String(input.value || '').trim();
|
||||||
|
if (value === '') return;
|
||||||
|
materials.push({
|
||||||
|
label: String(input.getAttribute('data-aksrt-after-sale-material') || ''),
|
||||||
|
value: value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return materials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeReason(shell) {
|
||||||
|
var parts = [];
|
||||||
|
var active = shell.querySelector('[data-aksrt-after-sale-reason].active');
|
||||||
|
if (active) {
|
||||||
|
var label = active.getAttribute('data-aksrt-after-sale-reason') || '';
|
||||||
|
if (label) parts.push(label);
|
||||||
|
}
|
||||||
|
var materials = [];
|
||||||
|
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-material]'), function (input) {
|
||||||
|
var value = String(input.value || '').trim();
|
||||||
|
if (value === '') return;
|
||||||
|
materials.push(String(input.getAttribute('data-aksrt-after-sale-material') || '') + ':' + value);
|
||||||
|
});
|
||||||
|
if (materials.length) parts.push(materials.join('\n'));
|
||||||
|
var reasonInput = shell.querySelector('[name="reason"]');
|
||||||
|
var reasonText = reasonInput ? String(reasonInput.value || '').trim() : '';
|
||||||
|
if (reasonText) parts.push(reasonText);
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearToast(id) {
|
||||||
|
var toast = document.getElementById(id);
|
||||||
|
if (toast) toast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交前的人机验证:场景未启用时无挂载点,run 直接 resolve {};
|
||||||
|
// 用户中途取消验证时 run resolve null,此时还原按钮并中止提交。
|
||||||
|
function runCaptcha(scene) {
|
||||||
|
if (!window.AKSRTCaptcha || typeof window.AKSRTCaptcha.run !== 'function') {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
return window.AKSRTCaptcha.run(scene).then(function (payload) {
|
||||||
|
return payload === null ? Promise.reject({ cancelled: true }) : (payload || {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitApply(shell) {
|
||||||
|
var form = shell.querySelector('[data-aksrt-after-sale-order]');
|
||||||
|
var orderNumber = form ? form.getAttribute('data-aksrt-after-sale-order') : '';
|
||||||
|
var reasonInput = shell.querySelector('[name="reason"]');
|
||||||
|
var reasonText = reasonInput ? String(reasonInput.value || '').trim() : '';
|
||||||
|
if (!reasonText) {
|
||||||
|
notyf('请填写申请说明', 'danger');
|
||||||
|
if (reasonInput) reasonInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 预置原因被后台清空时不强制选择;有配置则必须选中一项(与举报弹窗一致)。
|
||||||
|
if (shell.querySelector('[data-aksrt-after-sale-reason]') && !shell.querySelector('[data-aksrt-after-sale-reason].active')) {
|
||||||
|
notyf('请选择售后原因', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var submitBtn = shell.querySelector('[data-aksrt-after-sale-submit]');
|
||||||
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
|
notyf('正在提交售后申请…', 'load', 3000, 'aksrt-after-sale-submit');
|
||||||
|
|
||||||
|
var restoreButton = function () {
|
||||||
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
|
};
|
||||||
|
var sendApply = function (captchaPayload) {
|
||||||
|
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': NONCE,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.assign({
|
||||||
|
reason: composeReason(shell),
|
||||||
|
reason_label: selectedReasonLabel(shell),
|
||||||
|
materials: collectMaterials(shell),
|
||||||
|
description: reasonText,
|
||||||
|
images: collectImages(shell),
|
||||||
|
}, captchaPayload)),
|
||||||
|
})
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
window.AksrtModalRuntime.close('submit');
|
||||||
|
notyf('售后申请已提交', 'success');
|
||||||
|
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
||||||
|
window.AKSRTOrderCenter.refresh(orderNumber);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
restoreButton();
|
||||||
|
notyf((result && result.message) || '售后申请提交失败', 'danger');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
restoreButton();
|
||||||
|
notyf('网络错误,请重试', 'danger');
|
||||||
|
})
|
||||||
|
.then(function () { clearToast('aksrt-after-sale-submit'); });
|
||||||
|
};
|
||||||
|
|
||||||
|
runCaptcha('after_sale_apply').then(sendApply).catch(function (error) {
|
||||||
|
restoreButton();
|
||||||
|
clearToast('aksrt-after-sale-submit');
|
||||||
|
if (error && error.cancelled) return;
|
||||||
|
notyf('人机验证失败,请重试', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销售后:待审核工单置为已取消(DELETE 售后工单),成功后刷新订单列表与统计。
|
||||||
|
function submitWithdraw(shell, button) {
|
||||||
|
var orderNumber = button.getAttribute('data-aksrt-after-sale-withdraw-confirm') || '';
|
||||||
|
if (!orderNumber || button.disabled) return;
|
||||||
|
button.disabled = true;
|
||||||
|
notyf('正在撤销售后申请…', 'load', 3000, 'aksrt-after-sale-withdraw');
|
||||||
|
|
||||||
|
var restoreButton = function () {
|
||||||
|
button.disabled = false;
|
||||||
|
};
|
||||||
|
var sendWithdraw = function (captchaPayload) {
|
||||||
|
window.fetch(API_ROOT + '/orders/' + encodeURIComponent(orderNumber) + '/after-sale', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': NONCE,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(captchaPayload || {}),
|
||||||
|
})
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
window.AksrtModalRuntime.close('withdraw');
|
||||||
|
notyf('售后申请已撤销', 'success');
|
||||||
|
if (window.AKSRTOrderCenter && typeof window.AKSRTOrderCenter.refresh === 'function') {
|
||||||
|
window.AKSRTOrderCenter.refresh(orderNumber);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
restoreButton();
|
||||||
|
notyf((result && result.message) || '撤销失败,请重试', 'danger');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
restoreButton();
|
||||||
|
notyf('网络错误,请重试', 'danger');
|
||||||
|
})
|
||||||
|
.then(function () { clearToast('aksrt-after-sale-withdraw'); });
|
||||||
|
};
|
||||||
|
|
||||||
|
runCaptcha('after_sale_cancel').then(sendWithdraw).catch(function (error) {
|
||||||
|
restoreButton();
|
||||||
|
clearToast('aksrt-after-sale-withdraw');
|
||||||
|
if (error && error.cancelled) return;
|
||||||
|
notyf('人机验证失败,请重试', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindShell(shell) {
|
||||||
|
if (!shell) return;
|
||||||
|
|
||||||
|
shell.addEventListener('click', function (event) {
|
||||||
|
// 原因徽章:单选 active(与举报弹窗同语义,重复点击已选徽章保持选中)。
|
||||||
|
var chip = event.target.closest('[data-aksrt-after-sale-reason]');
|
||||||
|
if (chip) {
|
||||||
|
event.preventDefault();
|
||||||
|
Array.prototype.forEach.call(shell.querySelectorAll('[data-aksrt-after-sale-reason].active'), function (el) {
|
||||||
|
el.classList.remove('active');
|
||||||
|
});
|
||||||
|
chip.classList.add('active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addBtn = event.target.closest('[data-aksrt-after-sale-add]');
|
||||||
|
if (addBtn) {
|
||||||
|
event.preventDefault();
|
||||||
|
var fileInput = shell.querySelector('[data-aksrt-after-sale-file]');
|
||||||
|
if (fileInput) fileInput.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeBtn = event.target.closest('[data-aksrt-after-sale-image-remove]');
|
||||||
|
if (removeBtn) {
|
||||||
|
event.preventDefault();
|
||||||
|
var item = removeBtn.closest('.preview-item');
|
||||||
|
if (item) item.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var submitBtn = event.target.closest('[data-aksrt-after-sale-submit]');
|
||||||
|
if (submitBtn) {
|
||||||
|
event.preventDefault();
|
||||||
|
submitApply(shell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次申请售后:载荷已随按钮渲染。can_reapply 必然伴随 has_application=true,
|
||||||
|
// 直接走 openApplyFlow 会被 has_application 分流回「售后详细」打转;
|
||||||
|
// 清除标记落回申请表单分支(expired 超时效拦截仍然生效)。
|
||||||
|
var reapplyBtn = event.target.closest('[data-aksrt-after-sale-reapply]');
|
||||||
|
if (reapplyBtn) {
|
||||||
|
event.preventDefault();
|
||||||
|
var payload = parsePayload(reapplyBtn.getAttribute('data-aksrt-after-sale-payload'));
|
||||||
|
if (payload && payload.after_sale) payload.after_sale.has_application = false;
|
||||||
|
window.AksrtModalRuntime.close('replace');
|
||||||
|
openApplyFlow(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 撤销售后:入口在售后详细弹窗状态行(仅待审核),点击先弹二次确认。
|
||||||
|
var withdrawBtn = event.target.closest('[data-aksrt-after-sale-withdraw]');
|
||||||
|
if (withdrawBtn) {
|
||||||
|
event.preventDefault();
|
||||||
|
openWithdraw(withdrawBtn.getAttribute('data-aksrt-after-sale-withdraw') || '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var withdrawConfirm = event.target.closest('[data-aksrt-after-sale-withdraw-confirm]');
|
||||||
|
if (withdrawConfirm) {
|
||||||
|
event.preventDefault();
|
||||||
|
submitWithdraw(shell, withdrawConfirm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.addEventListener('change', function (event) {
|
||||||
|
var fileInput = event.target.closest('[data-aksrt-after-sale-file]');
|
||||||
|
if (!fileInput || !fileInput.files || !fileInput.files.length) return;
|
||||||
|
Array.prototype.forEach.call(fileInput.files, function (file) {
|
||||||
|
uploadImage(shell, file);
|
||||||
|
});
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAfterSaleButtons() {
|
||||||
|
if (document.documentElement.dataset.aksrtAfterSaleInitialized === '1') return;
|
||||||
|
document.documentElement.dataset.aksrtAfterSaleInitialized = '1';
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
var apply = event.target.closest('[data-aksrt-after-sale-apply]');
|
||||||
|
if (apply) {
|
||||||
|
event.preventDefault();
|
||||||
|
openApplyFlow(parsePayload(apply.getAttribute('data-aksrt-after-sale-apply')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var detail = event.target.closest('[data-aksrt-after-sale-detail]');
|
||||||
|
if (detail) {
|
||||||
|
event.preventDefault();
|
||||||
|
openDetailFlow(parsePayload(detail.getAttribute('data-aksrt-after-sale-detail')));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery(document).off('auto_fun.aksrt-after-sale').on('auto_fun.aksrt-after-sale', initAfterSaleButtons);
|
||||||
|
// 委托监听不依赖 DOM 扫描,脚本加载时立即挂上;auto_fun 钩子保留作幂等重扫。
|
||||||
|
initAfterSaleButtons();
|
||||||
|
})();
|
||||||
+107
-82
@@ -7,6 +7,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var instances = new WeakMap();
|
var instances = new WeakMap();
|
||||||
|
var liveStates = [];
|
||||||
|
|
||||||
function request(path, body, method, operationKey) {
|
function request(path, body, method, operationKey) {
|
||||||
var options = { method: method || "POST" };
|
var options = { method: method || "POST" };
|
||||||
@@ -105,24 +106,12 @@
|
|||||||
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
|
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatewayResultText(value) {
|
function colorfulHeader(title, icon) {
|
||||||
if (!value) return "";
|
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
||||||
var result = value;
|
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
|
||||||
if (typeof value === "string") {
|
'<div class="colorful-make"></div>' +
|
||||||
try { result = JSON.parse(value); } catch (_) { return String(value); }
|
'<div class="text-center"><div class="em2x"><i class="fa fas ' + escapeHtml(icon) + '" aria-hidden="true"></i></div>' +
|
||||||
}
|
'<div class="mt10 em12 padding-w10">' + escapeHtml(title) + '</div></div></div>';
|
||||||
if (!result || typeof result !== "object") return String(result);
|
|
||||||
var labels = { succeeded: "退款成功", failed: "渠道明确拒绝", pending: "渠道处理中", unknown: "结果待确认" };
|
|
||||||
var parts = [];
|
|
||||||
if (result.status) parts.push(labels[String(result.status)] || String(result.status));
|
|
||||||
if (result.message) parts.push(String(result.message));
|
|
||||||
if (result.refund_id) parts.push("退款单号 " + String(result.refund_id));
|
|
||||||
return parts.join(" · ") || "渠道暂未提供可读结果";
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterSaleAmount(value, order) {
|
|
||||||
var amount = String(value == null ? "" : value);
|
|
||||||
return order && order.is_points ? amount + " " + String(order.currency_symbol || "积分") : "¥" + amount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDetail(state, order, session) {
|
function renderDetail(state, order, session) {
|
||||||
@@ -135,6 +124,8 @@
|
|||||||
var meta = statusMeta(status);
|
var meta = statusMeta(status);
|
||||||
var timeLeft = Number(order.time_left || 0);
|
var timeLeft = Number(order.time_left || 0);
|
||||||
var html = "";
|
var html = "";
|
||||||
|
// 炫彩标题区:与售后弹窗统一(modal-mini + jb-blue),运行时不再渲染默认标题栏。
|
||||||
|
html += colorfulHeader("订单详情", "fa-receipt");
|
||||||
|
|
||||||
// 状态横幅:整块配色只由 --hero-accent 驱动,换状态只改一处变量。
|
// 状态横幅:整块配色只由 --hero-accent 驱动,换状态只改一处变量。
|
||||||
html += '<section class="aksrt-order-detail__hero is-' + escapeHtml(status) + '">';
|
html += '<section class="aksrt-order-detail__hero is-' + escapeHtml(status) + '">';
|
||||||
@@ -176,12 +167,51 @@
|
|||||||
var afterSales = Array.isArray(order.after_sales) ? order.after_sales : [];
|
var afterSales = Array.isArray(order.after_sales) ? order.after_sales : [];
|
||||||
if (afterSales.length) {
|
if (afterSales.length) {
|
||||||
html += '<section class="aksrt-order-detail__group"><h4 class="aksrt-order-detail__group-title">售后记录</h4><ul class="aksrt-order-detail__after-sales">';
|
html += '<section class="aksrt-order-detail__group"><h4 class="aksrt-order-detail__group-title">售后记录</h4><ul class="aksrt-order-detail__after-sales">';
|
||||||
afterSales.forEach(function (item) {
|
afterSales.forEach(function (item, index) {
|
||||||
var labels = { pending: "待审核", processing: "处理中", approved: "已通过", rejected: "已拒绝", cancelled: "已取消", completed: "已完成" };
|
var labels = { pending: "待审核", processing: "处理中", approved: "已通过", rejected: "已拒绝", cancelled: "已取消", completed: "已完成" };
|
||||||
html += "<li><strong>" + escapeHtml(labels[item.status] || item.status || "") + "</strong><span>" + escapeHtml(item.reason || "") + "</span><small>退款金额:" + escapeHtml(afterSaleAmount(item.refund_amount, order)) + "</small>";
|
var asStatus = String(item.status || "");
|
||||||
if (item.admin_note) html += "<small>处理备注:" + escapeHtml(item.admin_note) + "</small>";
|
var asLabel = labels[asStatus] || asStatus;
|
||||||
if (item.gateway_result) html += "<small>渠道结果:" + escapeHtml(gatewayResultText(item.gateway_result)) + "</small>";
|
html += '<li class="aksrt-order-detail__after-sale is-' + escapeHtml(asStatus) + '">';
|
||||||
if (item.status === "pending") html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-cancel="' + escapeHtml(order.order_number) + '">取消申请</button>';
|
html += '<div class="aksrt-order-detail__after-sale-head"><span class="aksrt-order-detail__after-sale-badge is-' + escapeHtml(asStatus) + '">' + escapeHtml(asLabel) + "</span>";
|
||||||
|
if (item.created_at) html += "<time>" + escapeHtml(item.created_at) + "</time>";
|
||||||
|
html += "</div>";
|
||||||
|
// 原因 / 申请理由:新数据走 apply_form 结构化字段;老数据无 apply_form 时兜底展示融合串。
|
||||||
|
var applyForm = item.apply_form && typeof item.apply_form === "object" ? item.apply_form : null;
|
||||||
|
if (applyForm) {
|
||||||
|
var reasonLabel = String(applyForm.reason_label || "");
|
||||||
|
var applyDesc = String(applyForm.description || "");
|
||||||
|
if (reasonLabel !== "") html += '<p class="aksrt-order-detail__after-sale-reason"><span class="aksrt-order-detail__after-sale-field">原因:</span>' + escapeHtml(reasonLabel) + "</p>";
|
||||||
|
if (applyDesc !== "") html += '<p class="aksrt-order-detail__after-sale-reason"><span class="aksrt-order-detail__after-sale-field">申请理由:</span>' + escapeHtml(applyDesc).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
|
||||||
|
} else if (item.reason) {
|
||||||
|
html += '<p class="aksrt-order-detail__after-sale-reason">' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
|
||||||
|
}
|
||||||
|
// 查看详细:逐记录构造售后详细弹窗载荷(after_sale.latest 指向本条记录);
|
||||||
|
// 再次申请仅对最新一条且订单级可再申请时开放(after_sales 按 id 倒序,首条即最新)。
|
||||||
|
var recordPayload = escapeHtml(JSON.stringify({
|
||||||
|
order_number: order.order_number || "",
|
||||||
|
product_name: order.product_name || "",
|
||||||
|
amount: order.amount || "0",
|
||||||
|
is_points: !!order.is_points,
|
||||||
|
currency_symbol: order.currency_symbol || "",
|
||||||
|
status_label: order.status_label || "",
|
||||||
|
after_sale: {
|
||||||
|
enabled: true,
|
||||||
|
has_application: true,
|
||||||
|
can_reapply: index === 0 && !!((order.after_sale || {}).can_reapply),
|
||||||
|
latest: {
|
||||||
|
status: asStatus,
|
||||||
|
reason: String(item.reason || ""),
|
||||||
|
apply_form: applyForm,
|
||||||
|
images: Array.isArray(item.images) ? item.images : [],
|
||||||
|
refund_amount: String(item.refund_amount || "0"),
|
||||||
|
created_at: String(item.created_at || ""),
|
||||||
|
handling_log: Array.isArray(item.handling_log) ? item.handling_log : [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
html += '<div class="aksrt-order-detail__after-sale-foot">';
|
||||||
|
html += '<button class="btn-ghost" type="button" data-aksrt-after-sale-detail="' + recordPayload + '">查看详细</button>';
|
||||||
|
html += "</div>";
|
||||||
html += "</li>";
|
html += "</li>";
|
||||||
});
|
});
|
||||||
html += "</ul></section>";
|
html += "</ul></section>";
|
||||||
@@ -195,11 +225,17 @@
|
|||||||
html += "</div>";
|
html += "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '<footer class="aksrt-modal__footer aksrt-order-detail__actions">';
|
// 底部按钮与售后详细弹窗同式:but-average modal-buts;「关闭」与「我知道了」
|
||||||
html += '<button class="btn-outline" type="button" data-aksrt-modal-close>关闭</button>';
|
// 同款(but c-blue),平铺观感由 orders.css 的 footer 按钮规则统一呈现。
|
||||||
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
html += '<footer class="aksrt-order-detail__actions but-average modal-buts">';
|
||||||
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
|
html += '<button type="button" class="but c-blue" data-aksrt-modal-close>关闭</button>';
|
||||||
if (allowedActions.can_after_sale) html += '<form class="aksrt-order-detail__after-sale-form" data-aksrt-after-sale-form="' + escapeHtml(order.order_number) + '"><label>申请售后原因<textarea name="reason" required maxlength="2000" rows="3"></textarea></label><button class="btn-ghost" type="submit">提交售后申请</button></form>';
|
if (allowedActions.can_cancel) html += '<button type="button" class="but" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||||
|
if (allowedActions.can_pay) html += '<button type="button" class="but c-blue" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml(paymentMethod.value || "") + '">继续支付</button>';
|
||||||
|
// 售后入口:未申请时底部保留「申请售后」;已有申请时入口收拢到售后记录卡片的「查看详细」。
|
||||||
|
var detailAfterSale = order.after_sale || {};
|
||||||
|
if (detailAfterSale.enabled && !detailAfterSale.has_application) {
|
||||||
|
html += '<button type="button" class="but c-blue" data-aksrt-after-sale-apply="' + afterSalePayload(order) + '">申请售后</button>';
|
||||||
|
}
|
||||||
html += "</footer>";
|
html += "</footer>";
|
||||||
|
|
||||||
session.content.innerHTML = html;
|
session.content.innerHTML = html;
|
||||||
@@ -213,7 +249,6 @@
|
|||||||
method: refresh ? "POST" : "GET",
|
method: refresh ? "POST" : "GET",
|
||||||
headers: actionKey ? { "Idempotency-Key": state.operationKeys[actionKey] } : {},
|
headers: actionKey ? { "Idempotency-Key": state.operationKeys[actionKey] } : {},
|
||||||
kind: "orders",
|
kind: "orders",
|
||||||
title: "订单详情",
|
|
||||||
className: "aksrt-order-detail-modal",
|
className: "aksrt-order-detail-modal",
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
cache: !refresh,
|
cache: !refresh,
|
||||||
@@ -275,10 +310,40 @@
|
|||||||
|
|
||||||
function actionHtml(order) {
|
function actionHtml(order) {
|
||||||
var allowedActions = order.allowed_actions || {};
|
var allowedActions = order.allowed_actions || {};
|
||||||
|
var afterSale = order.after_sale || {};
|
||||||
var html = '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">订单详情</button>';
|
var html = '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">订单详情</button>';
|
||||||
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
if (allowedActions.can_cancel) html += '<button class="btn-ghost" type="button" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
|
||||||
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
|
if (allowedActions.can_pay) html += '<button class="btn-primary" type="button" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((order.payment_method || {}).value || "") + '">继续支付</button>';
|
||||||
if (allowedActions.can_after_sale) html += '<button class="btn-outline" type="button" data-aksrt-order-detail="' + escapeHtml(order.order_number) + '">申请售后</button>';
|
// 售后入口:申请售后 / 售后详细走 order-after-sale.js 的独立弹窗。
|
||||||
|
if (afterSale.enabled) html += afterSaleButtons(order);
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 售后按钮(列表卡片与订单详情弹窗共用):已申请 → 售后详细,否则 → 申请售后。
|
||||||
|
// 载荷与 PHP 模板同构(order-after-sale.js 同步消费),点击即弹窗、无前置请求。
|
||||||
|
function afterSalePayload(order) {
|
||||||
|
var afterSale = order.after_sale || {};
|
||||||
|
return escapeHtml(JSON.stringify({
|
||||||
|
order_number: order.order_number || "",
|
||||||
|
product_name: order.product_name || "",
|
||||||
|
amount: order.amount || "0",
|
||||||
|
is_points: !!order.is_points,
|
||||||
|
currency_symbol: order.currency_symbol || "",
|
||||||
|
status_label: order.status_label || "",
|
||||||
|
after_sale: afterSale,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterSaleButtons(order) {
|
||||||
|
var afterSale = order.after_sale || {};
|
||||||
|
var html = "";
|
||||||
|
if (!afterSale.enabled) return html;
|
||||||
|
var payload = afterSalePayload(order);
|
||||||
|
if (afterSale.has_application) {
|
||||||
|
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-detail="' + payload + '">售后详细</button>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
html += '<button class="btn-outline" type="button" data-aksrt-after-sale-apply="' + payload + '">申请售后</button>';
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,42 +620,12 @@
|
|||||||
if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); }
|
if (cancel && root.contains(cancel)) { event.preventDefault(); closeOrder(state, cancel); }
|
||||||
};
|
};
|
||||||
state.onRootSubmit = function (event) {
|
state.onRootSubmit = function (event) {
|
||||||
var afterSaleForm = closest(event.target, "[data-aksrt-after-sale-form]");
|
|
||||||
if (afterSaleForm) {
|
|
||||||
event.preventDefault();
|
|
||||||
var number = afterSaleForm.getAttribute("data-aksrt-after-sale-form") || "";
|
|
||||||
var reason = afterSaleForm.querySelector("[name=reason]");
|
|
||||||
if (!number || !reason || !String(reason.value || "").trim()) return;
|
|
||||||
request(orderPath(number, "/after-sale"), { reason: String(reason.value).trim() }, "POST", window.AksrtRequest.createIdempotencyKey()).then(function () {
|
|
||||||
notyf("售后申请已提交", "success");
|
|
||||||
invalidateOrder(number);
|
|
||||||
openDetail(state, number);
|
|
||||||
refreshView(state);
|
|
||||||
}).catch(function (error) { notyf(errorMessage(error, "售后申请提交失败"), "danger"); });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var form = closest(event.target, "[data-aksrt-orders-search]");
|
var form = closest(event.target, "[data-aksrt-orders-search]");
|
||||||
if (!form || !root.contains(form)) return;
|
if (!form || !root.contains(form)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var input = form.querySelector("#aksrt-order-number");
|
var input = form.querySelector("#aksrt-order-number");
|
||||||
loadView(state, { orderNumber: input ? String(input.value || "").trim() : "", page: 1 });
|
loadView(state, { orderNumber: input ? String(input.value || "").trim() : "", page: 1 });
|
||||||
};
|
};
|
||||||
state.onDocumentSubmit = function (event) {
|
|
||||||
var afterSaleForm = closest(event.target, "[data-aksrt-after-sale-form]");
|
|
||||||
if (!afterSaleForm || root.contains(afterSaleForm) || event.__aksrtAfterSaleHandled) return;
|
|
||||||
var number = afterSaleForm.getAttribute("data-aksrt-after-sale-form") || "";
|
|
||||||
if (!activeDetail(state, number)) return;
|
|
||||||
event.__aksrtAfterSaleHandled = true;
|
|
||||||
event.preventDefault();
|
|
||||||
var reason = afterSaleForm.querySelector("[name=reason]");
|
|
||||||
if (!number || !reason || !String(reason.value || "").trim()) return;
|
|
||||||
request(orderPath(number, "/after-sale"), { reason: String(reason.value).trim() }, "POST", window.AksrtRequest.createIdempotencyKey()).then(function () {
|
|
||||||
notyf("售后申请已提交", "success");
|
|
||||||
invalidateOrder(number);
|
|
||||||
openDetail(state, number);
|
|
||||||
refreshView(state);
|
|
||||||
}).catch(function (error) { notyf(errorMessage(error, "售后申请提交失败"), "danger"); });
|
|
||||||
};
|
|
||||||
state.onModalClose = function (event) {
|
state.onModalClose = function (event) {
|
||||||
var detail = event && event.detail ? event.detail : {};
|
var detail = event && event.detail ? event.detail : {};
|
||||||
if (!state.detailSession || detail.session !== state.detailSession) return;
|
if (!state.detailSession || detail.session !== state.detailSession) return;
|
||||||
@@ -601,25 +636,7 @@
|
|||||||
state.detailReplacementSession = null;
|
state.detailReplacementSession = null;
|
||||||
};
|
};
|
||||||
state.onDocumentClick = function (event) {
|
state.onDocumentClick = function (event) {
|
||||||
var afterSaleCancel = closest(event.target, "[data-aksrt-after-sale-cancel]");
|
// 取消/撤销售后已移入售后详细弹窗(order-after-sale.js 的「撤销售后」入口)。
|
||||||
if (afterSaleCancel && !event.__aksrtAfterSaleCancelHandled) {
|
|
||||||
var afterSaleNumber = afterSaleCancel.getAttribute("data-aksrt-after-sale-cancel") || "";
|
|
||||||
if (activeDetail(state, afterSaleNumber)) {
|
|
||||||
event.__aksrtAfterSaleCancelHandled = true;
|
|
||||||
event.preventDefault();
|
|
||||||
afterSaleCancel.disabled = true;
|
|
||||||
request(orderPath(afterSaleNumber, "/after-sale"), undefined, "DELETE", window.AksrtRequest.createIdempotencyKey()).then(function () {
|
|
||||||
notyf("售后申请已取消", "success");
|
|
||||||
invalidateOrder(afterSaleNumber);
|
|
||||||
openDetail(state, afterSaleNumber);
|
|
||||||
refreshView(state);
|
|
||||||
}).catch(function (error) {
|
|
||||||
afterSaleCancel.disabled = false;
|
|
||||||
notyf(errorMessage(error, "取消申请失败"), "danger");
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
|
if (!state.detailSession || window.AksrtModalRuntime.getActive() !== state.detailSession.shell) return;
|
||||||
var copy = closest(event.target, "[data-aksrt-order-copy]");
|
var copy = closest(event.target, "[data-aksrt-order-copy]");
|
||||||
if (copy) {
|
if (copy) {
|
||||||
@@ -648,11 +665,11 @@
|
|||||||
|
|
||||||
root.addEventListener("click", state.onRootClick);
|
root.addEventListener("click", state.onRootClick);
|
||||||
root.addEventListener("submit", state.onRootSubmit);
|
root.addEventListener("submit", state.onRootSubmit);
|
||||||
document.addEventListener("submit", state.onDocumentSubmit);
|
|
||||||
document.addEventListener("aksrt:modal:close", state.onModalClose);
|
document.addEventListener("aksrt:modal:close", state.onModalClose);
|
||||||
document.addEventListener("click", state.onDocumentClick);
|
document.addEventListener("click", state.onDocumentClick);
|
||||||
state.countdownTimer = window.setInterval(function () { updateCountdowns(state); }, 1000);
|
state.countdownTimer = window.setInterval(function () { updateCountdowns(state); }, 1000);
|
||||||
instances.set(root, state);
|
instances.set(root, state);
|
||||||
|
liveStates.push(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rootsIn(root) {
|
function rootsIn(root) {
|
||||||
@@ -674,14 +691,22 @@
|
|||||||
if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace");
|
if (activeDetail(state, state.detailOrderNumber)) window.AksrtModalRuntime.close("replace");
|
||||||
item.removeEventListener("click", state.onRootClick);
|
item.removeEventListener("click", state.onRootClick);
|
||||||
item.removeEventListener("submit", state.onRootSubmit);
|
item.removeEventListener("submit", state.onRootSubmit);
|
||||||
document.removeEventListener("submit", state.onDocumentSubmit);
|
|
||||||
document.removeEventListener("aksrt:modal:close", state.onModalClose);
|
document.removeEventListener("aksrt:modal:close", state.onModalClose);
|
||||||
document.removeEventListener("click", state.onDocumentClick);
|
document.removeEventListener("click", state.onDocumentClick);
|
||||||
window.clearInterval(state.countdownTimer);
|
window.clearInterval(state.countdownTimer);
|
||||||
instances.delete(item);
|
instances.delete(item);
|
||||||
|
liveStates = liveStates.filter(function (entry) { return entry !== state; });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.AKSRTOrderCenter = { init: init, destroy: destroy };
|
// refresh 供售后弹窗(order-after-sale.js)提交/取消后刷新列表与统计。
|
||||||
|
function refresh(orderNumber) {
|
||||||
|
invalidateOrder(orderNumber);
|
||||||
|
liveStates.forEach(function (state) {
|
||||||
|
refreshView(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AKSRTOrderCenter = { init: init, destroy: destroy, refresh: refresh };
|
||||||
jQuery(document).off("auto_fun.aksrt-order-center").on("auto_fun.aksrt-order-center", function () { init(document); });
|
jQuery(document).off("auto_fun.aksrt-order-center").on("auto_fun.aksrt-order-center", function () { init(document); });
|
||||||
}());
|
}());
|
||||||
+110
-23
@@ -17,6 +17,14 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server-side schema sanitization limits these configured rich-text fields to post-safe HTML.
|
||||||
|
function htmlNode(tag, className, value) {
|
||||||
|
var el = document.createElement(tag);
|
||||||
|
if (className) el.className = className;
|
||||||
|
if (value !== undefined && value !== null) el.innerHTML = text(value);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function imageNode(src, className) {
|
function imageNode(src, className) {
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
var image = document.createElement("img");
|
var image = document.createElement("img");
|
||||||
@@ -61,7 +69,7 @@
|
|||||||
if (levelInfo && levelInfo.img_icon) {
|
if (levelInfo && levelInfo.img_icon) {
|
||||||
var img = node("img", "img-icon");
|
var img = node("img", "img-icon");
|
||||||
img.src = text(levelInfo.img_icon);
|
img.src = text(levelInfo.img_icon);
|
||||||
img.alt = text(levelInfo.name || "");
|
img.alt = text(levelInfo.name_text || levelInfo.name || "");
|
||||||
img.decoding = "async";
|
img.decoding = "async";
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
@@ -115,7 +123,8 @@
|
|||||||
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
||||||
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
||||||
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
||||||
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg jb-yellow", item.discount_tag));
|
// 标签颜色取后台「标签颜色」配置,未配置时回退参考主题默认的 jb-yellow。
|
||||||
|
if (item.discount_tag) append(product, htmlNode("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag));
|
||||||
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
|
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
|
||||||
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
||||||
append(label, product);
|
append(label, product);
|
||||||
@@ -179,7 +188,7 @@
|
|||||||
box.setAttribute("data-condition", "==");
|
box.setAttribute("data-condition", "==");
|
||||||
box.setAttribute("data-value", "card_pass");
|
box.setAttribute("data-value", "card_pass");
|
||||||
if (defaultMethod !== "card_pass") box.style.display = "none";
|
if (defaultMethod !== "card_pass") box.style.display = "none";
|
||||||
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
if (cardPass.desc) append(box, htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
||||||
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
||||||
if (!cardPass.only_password) {
|
if (!cardPass.only_password) {
|
||||||
var cardInput = node("input", "form-control mb6");
|
var cardInput = node("input", "form-control mb6");
|
||||||
@@ -384,12 +393,7 @@
|
|||||||
var avatarItem = node("li");
|
var avatarItem = node("li");
|
||||||
var avatarWrap = node("div", "avatar-img");
|
var avatarWrap = node("div", "avatar-img");
|
||||||
if (member.avatar_url) {
|
if (member.avatar_url) {
|
||||||
var avatar = node("img");
|
append(avatarWrap, imageNode(text(member.avatar_url), "avatar"));
|
||||||
avatar.src = text(member.avatar_url);
|
|
||||||
avatar.alt = "";
|
|
||||||
avatar.width = 42;
|
|
||||||
avatar.height = 42;
|
|
||||||
append(avatarWrap, avatar);
|
|
||||||
}
|
}
|
||||||
append(avatarItem, avatarWrap);
|
append(avatarItem, avatarWrap);
|
||||||
append(userBox, avatarItem);
|
append(userBox, avatarItem);
|
||||||
@@ -403,7 +407,7 @@
|
|||||||
} else if (member.state === "expired") {
|
} else if (member.state === "expired") {
|
||||||
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
||||||
} else if (catalog.desc) {
|
} else if (catalog.desc) {
|
||||||
append(userInfo, node("div", "muted-2-color em09", catalog.desc));
|
append(userInfo, htmlNode("div", "muted-2-color em09", catalog.desc));
|
||||||
}
|
}
|
||||||
append(userBox, userInfo);
|
append(userBox, userInfo);
|
||||||
append(modal, userBox);
|
append(modal, userBox);
|
||||||
@@ -439,8 +443,8 @@
|
|||||||
append(miniIcon, vipIcon(levelInfo));
|
append(miniIcon, vipIcon(levelInfo));
|
||||||
append(miniIconbox, miniIcon);
|
append(miniIconbox, miniIcon);
|
||||||
var miniInfo = node("div", "v-info");
|
var miniInfo = node("div", "v-info");
|
||||||
append(miniInfo, node("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
append(miniInfo, htmlNode("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
||||||
append(miniInfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
append(miniInfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||||
append(miniIconbox, miniInfo);
|
append(miniIconbox, miniInfo);
|
||||||
append(miniCard, miniIconbox);
|
append(miniCard, miniIconbox);
|
||||||
append(tab, miniCard);
|
append(tab, miniCard);
|
||||||
@@ -466,15 +470,12 @@
|
|||||||
append(vicon, vipIcon(levelInfo));
|
append(vicon, vipIcon(levelInfo));
|
||||||
append(iconbox, vicon);
|
append(iconbox, vicon);
|
||||||
var vinfo = node("div", "v-info");
|
var vinfo = node("div", "v-info");
|
||||||
append(vinfo, node("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
append(vinfo, htmlNode("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
||||||
append(vinfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
append(vinfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||||
append(iconbox, vinfo);
|
append(iconbox, vinfo);
|
||||||
append(info, iconbox);
|
append(info, iconbox);
|
||||||
if (levelInfo.equity) {
|
if (levelInfo.equity) {
|
||||||
var equity = document.createElement("ul");
|
append(info, htmlNode("div", "", levelInfo.equity));
|
||||||
equity.className = "v-equity";
|
|
||||||
equity.innerHTML = text(levelInfo.equity);
|
|
||||||
append(info, equity);
|
|
||||||
}
|
}
|
||||||
var infoBa = node("div", "abs-center vip-baicon");
|
var infoBa = node("div", "abs-center vip-baicon");
|
||||||
append(infoBa, vipIcon(levelInfo));
|
append(infoBa, vipIcon(levelInfo));
|
||||||
@@ -497,7 +498,7 @@
|
|||||||
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
||||||
append(products, pointsLink);
|
append(products, pointsLink);
|
||||||
}
|
}
|
||||||
if (catalog.more) append(products, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
if (catalog.more) append(products, htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||||
renderMethods(products, methods, cardPass, wallet);
|
renderMethods(products, methods, cardPass, wallet);
|
||||||
var actionName = actionKey(action);
|
var actionName = actionKey(action);
|
||||||
var actionAllowed = allowed[actionName] === true;
|
var actionAllowed = allowed[actionName] === true;
|
||||||
@@ -524,8 +525,6 @@
|
|||||||
append(modal, tabs);
|
append(modal, tabs);
|
||||||
append(modal, content);
|
append(modal, content);
|
||||||
|
|
||||||
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
|
|
||||||
|
|
||||||
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
|
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
|
||||||
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
||||||
applyVipMethodVisibility(pane);
|
applyVipMethodVisibility(pane);
|
||||||
@@ -703,6 +702,46 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 骨架屏:仿弹窗真实布局(用户框 + 等级卡 + 双列套餐面板)。
|
||||||
|
// 线条/色块复用全局骨架类(build/public.min.css),VIP 专属形状见 vip-modal.css。
|
||||||
|
function vipSkeleton() {
|
||||||
|
var skeleton = node("div", "aksrt-vip-modal-skeleton");
|
||||||
|
skeleton.setAttribute("aria-live", "polite");
|
||||||
|
skeleton.setAttribute("aria-busy", "true");
|
||||||
|
|
||||||
|
// 用户框:头像圆 + 两行文字。
|
||||||
|
var user = node("div", "aksrt-vip-modal-skeleton__user");
|
||||||
|
append(user, node("span", "aksrt-vip-modal-skeleton__avatar"));
|
||||||
|
var userLines = node("span", "aksrt-vip-modal-skeleton__lines");
|
||||||
|
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||||
|
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||||
|
append(user, userLines);
|
||||||
|
append(skeleton, user);
|
||||||
|
|
||||||
|
// 等级卡行(vip-cardminis):两张 tab 占位。
|
||||||
|
var tabs = node("div", "aksrt-vip-modal-skeleton__tabs");
|
||||||
|
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||||
|
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||||
|
append(skeleton, tabs);
|
||||||
|
|
||||||
|
// 双列面板:左权益信息 + 右套餐区(套餐卡、支付方式行、提交按钮条)。
|
||||||
|
var pane = node("div", "aksrt-vip-modal-skeleton__pane");
|
||||||
|
var info = node("div", "aksrt-vip-modal-skeleton__info");
|
||||||
|
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||||
|
append(info, node("span", "aksrt-modal-runtime__skeleton-line"));
|
||||||
|
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||||
|
append(pane, info);
|
||||||
|
var products = node("div", "aksrt-vip-modal-skeleton__products");
|
||||||
|
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||||
|
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||||
|
append(products, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||||
|
append(products, node("span", "aksrt-vip-modal-skeleton__button"));
|
||||||
|
append(pane, products);
|
||||||
|
append(skeleton, pane);
|
||||||
|
|
||||||
|
return skeleton.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function vipPay(trigger) {
|
function vipPay(trigger) {
|
||||||
var level = trigger
|
var level = trigger
|
||||||
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
||||||
@@ -711,11 +750,11 @@
|
|||||||
url: "/pay/vip/modal",
|
url: "/pay/vip/modal",
|
||||||
query: { vip_level: level },
|
query: { vip_level: level },
|
||||||
className: "aksrt-vip-modal-shell",
|
className: "aksrt-vip-modal-shell",
|
||||||
skeleton: '<div class="aksrt-vip-modal-panel"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>',
|
skeleton: vipSkeleton(),
|
||||||
telemetry: "vip",
|
telemetry: "vip",
|
||||||
kind: "vip",
|
kind: "vip",
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
cacheTtl: 15000,
|
cacheTtl: 60000,
|
||||||
cacheTags: ["vip"],
|
cacheTags: ["vip"],
|
||||||
renderData: function (data, session) {
|
renderData: function (data, session) {
|
||||||
renderModalView(session.content, data);
|
renderModalView(session.content, data);
|
||||||
@@ -733,6 +772,53 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prefetchedLevels = Object.create(null);
|
||||||
|
|
||||||
|
// 与 openRemote 同一 cacheKey(method + AksrtRequest.url),命中同一份缓存/flight。
|
||||||
|
function vipModalCacheKey(level) {
|
||||||
|
return "GET:" + window.AksrtRequest.url("/pay/vip/modal", { vip_level: level });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 意图预热:悬停/聚焦开通入口时按等级预热弹窗数据(不打开弹窗,失败静默)。
|
||||||
|
// 点击打开时若预热请求在途,cache.acquire 的 flight 去重会自动合并。
|
||||||
|
function prefetchVipModal(trigger) {
|
||||||
|
if (!trigger || !window.AksrtModalCache || !window.AksrtRequest) return;
|
||||||
|
var level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
|
||||||
|
if (level < 1 || level > 2) level = 1;
|
||||||
|
if (prefetchedLevels[level]) return;
|
||||||
|
prefetchedLevels[level] = true;
|
||||||
|
var cache = window.AksrtModalCache;
|
||||||
|
if (cache.get && cache.get(vipModalCacheKey(level)).hit) return;
|
||||||
|
try {
|
||||||
|
cache.acquire(vipModalCacheKey(level), function () {
|
||||||
|
return window.AksrtRequest.request("/pay/vip/modal", {
|
||||||
|
method: "GET",
|
||||||
|
query: { vip_level: level },
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
ttl: 60000,
|
||||||
|
tags: ["vip"],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 预热失败不影响点击后的正常打开路径。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function vipTriggerFrom(event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.closest) return null;
|
||||||
|
return target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPrefetch() {
|
||||||
|
// pointerenter 不冒泡但走捕获阶段;focusin 冒泡,两者互补覆盖鼠标与键盘。
|
||||||
|
["pointerenter", "focusin"].forEach(function (type) {
|
||||||
|
document.addEventListener(type, function (event) {
|
||||||
|
prefetchVipModal(vipTriggerFrom(event));
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onDocClick(event) {
|
function onDocClick(event) {
|
||||||
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||||
if (!trigger) return;
|
if (!trigger) return;
|
||||||
@@ -773,6 +859,7 @@
|
|||||||
if (window.__aksrtPayVipBound) return;
|
if (window.__aksrtPayVipBound) return;
|
||||||
window.__aksrtPayVipBound = true;
|
window.__aksrtPayVipBound = true;
|
||||||
document.addEventListener("click", onDocClick, true);
|
document.addEventListener("click", onDocClick, true);
|
||||||
|
bindPrefetch();
|
||||||
bindPointsModal();
|
bindPointsModal();
|
||||||
bindPointsEntry();
|
bindPointsEntry();
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
(function($,window2,document2){"use strict";function closeNotyf($element){$element.addClass("notyn-out"),window2.setTimeout(function(){$element.remove()},1e3)}function notyf2(message,type,time,id){var $body=jQuery(document2.body);jQuery(".notyn").length||$body.append('<div class="notyn"></div>'),type=type||"success",type==="error"&&(type="danger"),time=time||5e3,time=time<100?time*1e3:time;var idAttribute=id?' id="'+id+'"':"",$notification=jQuery('<div class="noty1"'+idAttribute+'><div class="notyf '+type+'">'+message+"</div></div>"),shouldClose=!id;id&&jQuery("#"+id).length?(jQuery("#"+id).find(".notyf").removeClass().addClass("notyf "+type).html(message),$notification=jQuery("#"+id),shouldClose=!0):jQuery(".notyn").append($notification),shouldClose&&window2.setTimeout(function(){closeNotyf($notification)},time)}jQuery(document2.body).on("click",".noty1",function(){closeNotyf(jQuery(this))}),window2.notyf=notyf2})(jQuery,window,document),(function(window2){"use strict";var allowedKinds={identity:!0,badges:!0,orders:!0,vip:!0},allowedEvents={click:!0,shell:!0,"first-content":!0,ready:!0,"action-start":!0,"action-end":!0,unauthorized:!0,error:!0},sessions=Object.create(null),sequence=0,maxSessions=100;function normalize(value,fallback){var normalized=String(value||fallback||"").toLowerCase().replace(/[^a-z0-9-]/g,"");return normalized||fallback}function now(){return window2.performance&&typeof window2.performance.now=="function"?window2.performance.now():0}function markName(session,event){return"aksrt-modal-"+session.kind+"-"+session.id+"-"+event}function emit(session,event){if(!(!window2.performance||typeof window2.performance.mark!="function")){var name=markName(session,event);if(window2.performance.mark(name),session.marks[event]=name,typeof window2.performance.measure=="function"&&session.marks.click&&event!=="click")try{window2.performance.measure("aksrt-modal-"+session.kind+"-"+session.id+"-click-to-"+event,session.marks.click,name)}catch{}}}function start(kind){var safeKind=normalize(kind,"unknown");allowedKinds[safeKind]||(safeKind="unknown"),sequence+=1;var id=String(sequence),session={id,kind:safeKind,startedAt:now(),marks:Object.create(null)};sessions[id]=session,emit(session,"click");for(var ids=Object.keys(sessions);ids.length>maxSessions;)delete sessions[ids.shift()];return id}function mark(id,event){var session=sessions[String(id||"")],safeEvent=normalize(event,"error");!session||!allowedEvents[safeEvent]||emit(session,safeEvent)}function finish(id){mark(id,"ready")}function fail(id){mark(id,"error")}window2.AksrtModalTelemetry={start,mark,finish,error:fail}})(window),(function(window2){"use strict";function data(){return window2.aksrtData||window2.aksrt||{}}function createIdempotencyKey(){if(window2.crypto&&typeof window2.crypto.randomUUID=="function")return window2.crypto.randomUUID();if(!window2.crypto||typeof window2.crypto.getRandomValues!="function")throw new Error("当前浏览器无法生成安全的操作标识");var bytes=new Uint8Array(16);window2.crypto.getRandomValues(bytes),bytes[6]=bytes[6]&15|64,bytes[8]=bytes[8]&63|128;var hex=Array.prototype.map.call(bytes,function(value){return value.toString(16).padStart(2,"0")}).join("");return hex.slice(0,8)+"-"+hex.slice(8,12)+"-"+hex.slice(12,16)+"-"+hex.slice(16,20)+"-"+hex.slice(20)}function restBase(){var config=data(),raw=config.restUrl||config.apiUrl||config.rest_url||"",siteUrl=config.siteUrl||config.homeUrl||window2.location.origin+"/";return window2.location.protocol==="https:"&&siteUrl.substring(0,5)==="http:"&&(siteUrl="https:"+siteUrl.substring(5)),raw?String(raw).replace(/\/$/,""):String(siteUrl).replace(/\/$/,"")+"/index.php?rest_route=/aksrt/v1"}function appendQuery(url2,query){if(!query)return url2;var parts=[];return typeof query=="string"?parts=query.replace(/^\?/,"").split("&").filter(Boolean):typeof query=="object"&&Object.keys(query).forEach(function(key){query[key]===void 0||query[key]===null||parts.push(encodeURIComponent(key)+"="+encodeURIComponent(String(query[key])))}),parts.length?url2+(url2.indexOf("?")>=0?"&":"?")+parts.join("&"):url2}function url(path,query){path=String(path||"");var inlineQuery="",queryIndex=path.indexOf("?");if(queryIndex>=0&&(inlineQuery=path.slice(queryIndex+1),path=path.slice(0,queryIndex)),/^https?:\/\//i.test(path))return appendQuery(path,query||inlineQuery);path.charAt(0)!=="/"&&(path="/"+path);var base=restBase(),result=(base.indexOf("rest_route=")>=0,base+path);return appendQuery(result,query||inlineQuery)}function headers(options){var config=data(),source=options&&options.headers||{},result={},nonce=config.restNonce||config.nonce||"";return Object.keys(source).forEach(function(key){result[key]=source[key]}),result.Accept||(result.Accept="application/json, text/html;q=0.9, */*;q=0.8"),nonce&&!result["X-WP-Nonce"]&&(result["X-WP-Nonce"]=nonce),options&&options.body!==void 0&&options.body!==null&&!(window2.FormData&&options.body instanceof window2.FormData)&&!result["Content-Type"]&Line truncated
|
(function($,window2,document2){"use strict";function closeNotyf($element){$element.addClass("notyn-out"),window2.setTimeout(function(){$element.remove()},1e3)}function notyf2(message,type,time,id){var $body=jQuery(document2.body);jQuery(".notyn").length||$body.append('<div class="notyn"></div>'),type=type||"success",type==="error"&&(type="danger"),time=time||5e3,time=time<100?time*1e3:time;var idAttribute=id?' id="'+id+'"':"",$notification=jQuery('<div class="noty1"'+idAttribute+'><div class="notyf '+type+'">'+message+"</div></div>"),shouldClose=!id;id&&jQuery("#"+id).length?(jQuery("#"+id).find(".notyf").removeClass().addClass("notyf "+type).html(message),$notification=jQuery("#"+id),shouldClose=!0):jQuery(".notyn").append($notification),shouldClose&&window2.setTimeout(function(){closeNotyf($notification)},time)}jQuery(document2.body).on("click",".noty1",function(){closeNotyf(jQuery(this))}),window2.notyf=notyf2})(jQuery,window,document),(function(window2){"use strict";var allowedKinds={identity:!0,badges:!0,orders:!0,vip:!0},allowedEvents={click:!0,shell:!0,"first-content":!0,ready:!0,"action-start":!0,"action-end":!0,unauthorized:!0,error:!0},sessions=Object.create(null),sequence=0,maxSessions=100;function normalize(value,fallback){var normalized=String(value||fallback||"").toLowerCase().replace(/[^a-z0-9-]/g,"");return normalized||fallback}function now(){return window2.performance&&typeof window2.performance.now=="function"?window2.performance.now():0}function markName(session,event){return"aksrt-modal-"+session.kind+"-"+session.id+"-"+event}function emit(session,event){if(!(!window2.performance||typeof window2.performance.mark!="function")){var name=markName(session,event);if(window2.performance.mark(name),session.marks[event]=name,typeof window2.performance.measure=="function"&&session.marks.click&&event!=="click")try{window2.performance.measure("aksrt-modal-"+session.kind+"-"+session.id+"-click-to-"+event,session.marks.click,name)}catch{}}}function start(kind){var safeKind=normalize(kind,"unknown");allowedKinds[safeKind]||(safeKind="unknown"),sequence+=1;var id=String(sequence),session={id,kind:safeKind,startedAt:now(),marks:Object.create(null)};sessions[id]=session,emit(session,"click");for(var ids=Object.keys(sessions);ids.length>maxSessions;)delete sessions[ids.shift()];return id}function mark(id,event){var session=sessions[String(id||"")],safeEvent=normalize(event,"error");!session||!allowedEvents[safeEvent]||emit(session,safeEvent)}function finish(id){mark(id,"ready")}function fail(id){mark(id,"error")}window2.AksrtModalTelemetry={start,mark,finish,error:fail}})(window),(function(window2){"use strict";function data(){return window2.aksrtData||window2.aksrt||{}}function createIdempotencyKey(){if(window2.crypto&&typeof window2.crypto.randomUUID=="function")return window2.crypto.randomUUID();if(!window2.crypto||typeof window2.crypto.getRandomValues!="function")throw new Error("当前浏览器无法生成安全的操作标识");var bytes=new Uint8Array(16);window2.crypto.getRandomValues(bytes),bytes[6]=bytes[6]&15|64,bytes[8]=bytes[8]&63|128;var hex=Array.prototype.map.call(bytes,function(value){return value.toString(16).padStart(2,"0")}).join("");return hex.slice(0,8)+"-"+hex.slice(8,12)+"-"+hex.slice(12,16)+"-"+hex.slice(16,20)+"-"+hex.slice(20)}function restBase(){var config=data(),raw=config.restUrl||config.apiUrl||config.rest_url||"",siteUrl=config.siteUrl||config.homeUrl||window2.location.origin+"/";return window2.location.protocol==="https:"&&siteUrl.substring(0,5)==="http:"&&(siteUrl="https:"+siteUrl.substring(5)),raw?String(raw).replace(/\/$/,""):String(siteUrl).replace(/\/$/,"")+"/index.php?rest_route=/aksrt/v1"}function appendQuery(url2,query){if(!query)return url2;var parts=[];return typeof query=="string"?parts=query.replace(/^\?/,"").split("&").filter(Boolean):typeof query=="object"&&Object.keys(query).forEach(function(key){query[key]===void 0||query[key]===null||parts.push(encodeURIComponent(key)+"="+encodeURIComponent(String(query[key])))}),parts.length?url2+(url2.indexOf("?")>=0?"&":"?")+parts.join("&"):url2}function url(path,query){path=String(path||"");var inlineQuery="",queryIndex=path.indexOf("?");if(queryIndex>=0&&(inlineQuery=path.slice(queryIndex+1),path=path.slice(0,queryIndex)),/^https?:\/\//i.test(path))return appendQuery(path,query||inlineQuery);path.charAt(0)!=="/"&&(path="/"+path);var base=restBase(),result=(base.indexOf("rest_route=")>=0,base+path);return appendQuery(result,query||inlineQuery)}function headers(options){var config=data(),source=options&&options.headers||{},result={},nonce=config.restNonce||config.nonce||"";return Object.keys(source).forEach(function(key){result[key]=source[key]}),result.Accept||(result.Accept="application/json, text/html;q=0.9, */*;q=0.8"),nonce&&!result["X-WP-Nonce"]&&(result["X-WP-Nonce"]=nonce),options&&options.body!==void 0&&options.body!==null&&!(window2.FormData&&options.body instanceof window2.FormData)&&!result["Content-Type"]&Line truncated
|
||||||
@@ -377,7 +377,7 @@
|
|||||||
if (data.redirect && (data.payment_url || data.qr_content)) {
|
if (data.redirect && (data.payment_url || data.qr_content)) {
|
||||||
callback(options, "onPending", data);
|
callback(options, "onPending", data);
|
||||||
if (!data.order_number) {
|
if (!data.order_number) {
|
||||||
var missing = { ok: false, status: 500, json: { message: "流水号缺失" } };
|
var missing = { ok: false, status: 500, json: { message: "订单号缺失" } };
|
||||||
callback(options, "onError", missing);
|
callback(options, "onError", missing);
|
||||||
return missing;
|
return missing;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
|
|||||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||||
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
|
||||||
|
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
|
||||||
|
}
|
||||||
if (class_exists('\AKSRT\Core\SEOHead')) {
|
if (class_exists('\AKSRT\Core\SEOHead')) {
|
||||||
\AKSRT\Core\SEOHead::get_instance()->init();
|
\AKSRT\Core\SEOHead::get_instance()->init();
|
||||||
}
|
}
|
||||||
@@ -406,9 +409,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) {
|
|||||||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayProducts')) {
|
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayProducts')) {
|
||||||
\AKSRT\Admin\Native\PayProducts::get_instance()->init();
|
\AKSRT\Admin\Native\PayProducts::get_instance()->init();
|
||||||
}
|
}
|
||||||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayDownloadLogs')) {
|
|
||||||
\AKSRT\Admin\Native\PayDownloadLogs::get_instance()->init();
|
|
||||||
}
|
|
||||||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayWithdrawals')) {
|
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayWithdrawals')) {
|
||||||
\AKSRT\Admin\Native\PayWithdrawals::get_instance()->init();
|
\AKSRT\Admin\Native\PayWithdrawals::get_instance()->init();
|
||||||
}
|
}
|
||||||
@@ -416,6 +416,11 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayIncome')) {
|
|||||||
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
|
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后台文章列表增强:互动/销售数据列 + 快捷编辑配置项
|
||||||
|
if (is_admin() && class_exists('\AKSRT\Admin\Native\PostListEnhance')) {
|
||||||
|
\AKSRT\Admin\Native\PostListEnhance::get_instance()->init();
|
||||||
|
}
|
||||||
|
|
||||||
// WordPress 原生用户菜单:消息/推广码/认证/禁封(legacy users.php 子菜单对齐)
|
// WordPress 原生用户菜单:消息/推广码/认证/禁封(legacy users.php 子菜单对齐)
|
||||||
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
|
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
|
||||||
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
|
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
|
||||||
|
|||||||
@@ -459,65 +459,6 @@ add_filter('wp_kses_allowed_html', function ($allowed, $context) {
|
|||||||
return $allowed;
|
return $allowed;
|
||||||
}, 10, 2);
|
}, 10, 2);
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// WP 后台文章列表增强列 — 浏览/点赞/收藏/下载
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
add_filter('manage_post_posts_columns', function ($columns) {
|
|
||||||
$new_columns = [];
|
|
||||||
foreach ($columns as $key => $label) {
|
|
||||||
$new_columns[$key] = $label;
|
|
||||||
if ($key === 'title') {
|
|
||||||
$new_columns['aksrt_stats'] = __('互动数据', 'aksrt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $new_columns;
|
|
||||||
});
|
|
||||||
|
|
||||||
add_action('admin_enqueue_scripts', function ($hook) {
|
|
||||||
if ($hook !== 'edit.php') return;
|
|
||||||
aksrt_enqueue_fontawesome();
|
|
||||||
});
|
|
||||||
|
|
||||||
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
|
|
||||||
if ($column !== 'aksrt_stats') return;
|
|
||||||
|
|
||||||
$views = (int) get_post_meta($post_id, 'views', true);
|
|
||||||
$likes = (int) get_post_meta($post_id, 'like', true);
|
|
||||||
$favorites = (int) get_post_meta($post_id, 'favorite', true);
|
|
||||||
$downloads = (int) get_post_meta($post_id, 'download_count', true);
|
|
||||||
|
|
||||||
$format = function (int $n): string {
|
|
||||||
if ($n >= 10000) return round($n / 10000, 1) . '万';
|
|
||||||
if ($n >= 1000) return round($n / 1000, 1) . 'k';
|
|
||||||
return (string) $n;
|
|
||||||
};
|
|
||||||
|
|
||||||
$parts = [];
|
|
||||||
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
|
|
||||||
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
|
|
||||||
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
|
|
||||||
|
|
||||||
if ($downloads > 0) {
|
|
||||||
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo implode(' · ', $parts);
|
|
||||||
}, 10, 2);
|
|
||||||
|
|
||||||
// Support sorting by views/likes/favorites in admin
|
|
||||||
add_action('pre_get_posts', function ($query) {
|
|
||||||
if (!is_admin() || !$query->is_main_query()) return;
|
|
||||||
|
|
||||||
$orderby = $query->get('orderby');
|
|
||||||
$allowed = ['views', 'like', 'favorite', 'download_count'];
|
|
||||||
|
|
||||||
if (in_array($orderby, $allowed, true)) {
|
|
||||||
$query->set('meta_key', $orderby);
|
|
||||||
$query->set('orderby', 'meta_value_num');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Gutenberg 自定义块注册
|
// Gutenberg 自定义块注册
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ if (! function_exists('aksrt_format_number')) :
|
|||||||
if ($num >= 10000) {
|
if ($num >= 10000) {
|
||||||
return round($num / 10000, 1) . 'W+';
|
return round($num / 10000, 1) . 'W+';
|
||||||
} elseif ($num >= 1000) {
|
} elseif ($num >= 1000) {
|
||||||
return round($num / 1000, 1) . 'k';
|
return round($num / 1000, 1) . 'K';
|
||||||
}
|
}
|
||||||
return (string) number_format_i18n($num);
|
return (string) number_format_i18n($num);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
|
|||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_normalize_header_cta_label')) :
|
|
||||||
/**
|
|
||||||
* Normalize CTA label HTML: map FA5 classes and keep a single leading icon.
|
|
||||||
*/
|
|
||||||
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fa fas fa-pen'): string {
|
|
||||||
$label = trim($label);
|
|
||||||
if ($label === '') {
|
|
||||||
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stripos($label, '<i') === false) {
|
|
||||||
// No space between icon and label — spacing via CSS margin (legacy .but > .fa).
|
|
||||||
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i><span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
|
|
||||||
} else {
|
|
||||||
$label = preg_replace(
|
|
||||||
'/(<\/i>)\s*([^<].*)$/u',
|
|
||||||
'$1<span class="navbar-cta-text">$2</span>',
|
|
||||||
$label
|
|
||||||
) ?? $label;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $label;
|
|
||||||
}
|
|
||||||
endif;
|
|
||||||
|
|
||||||
if (!function_exists('aksrt_render_new_add_btns')) :
|
if (!function_exists('aksrt_render_new_add_btns')) :
|
||||||
/**
|
/**
|
||||||
* Multi-type publish button (legacy newadd-btns).
|
* Publish chip (legacy newadd-btns, single action).
|
||||||
|
*
|
||||||
|
* Mirrors the single-button branch of the reference zib_get_new_add_btns():
|
||||||
|
* the paint classes ride on the anchor itself and the label is emitted
|
||||||
|
* verbatim. Only the post action exists, so there is no dropdown branch.
|
||||||
*
|
*
|
||||||
* @param string[] $types
|
* @param string[] $types
|
||||||
*/
|
*/
|
||||||
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$actions = aksrt_get_new_add_actions($types);
|
$actions = aksrt_get_new_add_actions($types);
|
||||||
if (!$actions) {
|
if (!$actions) {
|
||||||
// Last resort: keep a write-post entry so the top-nav chip is never blank.
|
return '';
|
||||||
$fallback = function_exists('aksrt_get_new_post_url') ? (string) aksrt_get_new_post_url() : '';
|
|
||||||
if ($fallback === '') {
|
|
||||||
$fallback = admin_url('post-new.php');
|
|
||||||
}
|
|
||||||
$actions = [[
|
|
||||||
'type' => 'post',
|
|
||||||
'url' => $fallback,
|
|
||||||
'label' => __('写文章', 'aksrt'),
|
|
||||||
'icon' => 'fa fas fa-file-alt',
|
|
||||||
]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$a = $actions[0];
|
||||||
if ($label === '') {
|
if ($label === '') {
|
||||||
$label = __('发布', 'aksrt');
|
$label = __('发布', 'aksrt');
|
||||||
}
|
}
|
||||||
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
|
|
||||||
|
|
||||||
// Split paint classes vs wrapper tokens.
|
return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
|
||||||
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
. $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
|
||||||
$cta_tokens = [];
|
|
||||||
$wrap_tokens = ['newadd-btns'];
|
|
||||||
foreach ($class_tokens as $token) {
|
|
||||||
if ($token === 'newadd-btns') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($token === 'btn-primary' || $token === 'but' || $token === 'navbar-cta' || $token === 'radius' || $token === 'main-radius' || $token === 'mini-radius' || str_starts_with($token, 'jb-') || str_starts_with($token, 'b-')) {
|
|
||||||
$cta_tokens[] = $token;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$wrap_tokens[] = $token;
|
|
||||||
}
|
|
||||||
if (!$cta_tokens) {
|
|
||||||
$cta_tokens[] = 'jb-blue';
|
|
||||||
}
|
|
||||||
if (!in_array('but', $cta_tokens, true)) {
|
|
||||||
$cta_tokens[] = 'but';
|
|
||||||
}
|
|
||||||
$cta_class = trim(implode(' ', array_unique($cta_tokens)));
|
|
||||||
|
|
||||||
if (count($actions) === 1) {
|
|
||||||
$a = $actions[0];
|
|
||||||
return '<a rel="nofollow" class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' ' . $cta_class . ' navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
|
|
||||||
. $label_html
|
|
||||||
. '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacy: span.newadd-btns.hover-show > toggle chip + .hover-show-con.drop-newadd
|
|
||||||
// Paint classes (jb-*, but, radius) MUST live on the toggle — wrapper is layout-only
|
|
||||||
// (background forced transparent). Bare navbar-but has no .navbar-but-cluster shell.
|
|
||||||
// data-aksrt-newadd: JS may portal menu to body so chip width cannot track menu.
|
|
||||||
$html = '<span class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' hover-show')) . '" data-aksrt-newadd="1">';
|
|
||||||
$html .= '<span class="newadd-btns__toggle navbar-cta ' . esc_attr($cta_class) . '">' . $label_html
|
|
||||||
. '<i class="fa fas fa-caret-down" aria-hidden="true"></i></span>';
|
|
||||||
$html .= '<div class="hover-show-con dropdown-menu drop-newadd" role="menu">';
|
|
||||||
foreach ($actions as $a) {
|
|
||||||
$html .= '<a rel="nofollow" class="btn-newadd newadd-btns__item" role="menuitem" href="' . esc_url($a['url']) . '">';
|
|
||||||
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
|
|
||||||
. '<text>' . esc_html($a['label']) . '</text>';
|
|
||||||
$html .= '</a>';
|
|
||||||
}
|
|
||||||
$html .= '</div></span>';
|
|
||||||
return $html;
|
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
*/
|
*/
|
||||||
function aksrt_render_header_menu_buttons(): string {
|
function aksrt_render_header_menu_buttons(): string {
|
||||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
||||||
$cfg = [
|
$enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
|
||||||
'enabled' => !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']),
|
$text = trim((string) ($settings['nav_newposts_text'] ?? ''));
|
||||||
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))),
|
$pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
|
||||||
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']),
|
$skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
|
||||||
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
|
|
||||||
];
|
|
||||||
$html = '';
|
$html = '';
|
||||||
if (!empty($cfg['enabled'])) {
|
if ($enabled) {
|
||||||
// legacy default: but nowave jb-blue (+ radius).
|
// legacy default: but nowave jb-blue (+ radius).
|
||||||
$class = 'but nowave navbar-newposts';
|
$class = 'but nowave ' . $skin;
|
||||||
$skin = trim((string) ($cfg['class'] ?? ''));
|
|
||||||
if ($skin === '') {
|
|
||||||
$skin = 'jb-blue';
|
|
||||||
}
|
|
||||||
$class .= ' ' . $skin;
|
|
||||||
$pill = !empty($cfg['radius_pill']);
|
|
||||||
if ($pill && !str_contains($class, 'radius')) {
|
if ($pill && !str_contains($class, 'radius')) {
|
||||||
$class .= ' radius';
|
$class .= ' radius';
|
||||||
}
|
}
|
||||||
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt'));
|
$label = $text !== '' ? $text : __('发布', 'aksrt');
|
||||||
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
||||||
if ($btn !== '') {
|
if ($btn !== '') {
|
||||||
$html .= $btn;
|
$html .= $btn;
|
||||||
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
$show_vip = false;
|
$show_vip = false;
|
||||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
||||||
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip']);
|
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip'])
|
||||||
|
&& !\AKSRT\Settings\Settings::close_sign();
|
||||||
}
|
}
|
||||||
if ($show_vip) {
|
if ($show_vip) {
|
||||||
$is_vip = false;
|
$is_vip = false;
|
||||||
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
if (method_exists($svc, 'render_vip_card')) {
|
if (method_exists($svc, 'render_vip_card')) {
|
||||||
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
||||||
if ($cards !== '') {
|
if ($cards !== '') {
|
||||||
$hover = '<div class="hover-show-con sub-menu sub-vip-card">' . $cards . '</div>';
|
$hover = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// legacy: span.hover-show wraps button + .hover-show-con.sub-vip-card
|
// legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
|
||||||
$html .= '<span class="hover-show inline-block navbar-vip-hover">';
|
$html .= '<span class="hover-show inline-block">';
|
||||||
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"'
|
$html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
|
||||||
. ($user_id > 0 ? ' vip-level="1"' : '') . '>';
|
. '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
. esc_html__('开通会员', 'aksrt') . '</a>' . $hover . '</span>';
|
||||||
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
|
|
||||||
$html .= '</a>' . $hover . '</span>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
* Check-in CTA for header dropdown and compact card placements.
|
* Check-in CTA for header dropdown and compact card placements.
|
||||||
*
|
*
|
||||||
* @param int $user_id Current user ID.
|
* @param int $user_id Current user ID.
|
||||||
* @param bool $compact Render a short card-friendly label.
|
|
||||||
* @param string|null $class Override the classes and render the reference-parity
|
* @param string|null $class Override the classes and render the reference-parity
|
||||||
* <a> badge (user-card 签到书签); null keeps the header
|
* <a> badge (user-card 签到书签); null keeps the header
|
||||||
* dropdown <button> runtime.
|
* dropdown <a> runtime.
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_checkin_btn(int $user_id = 0, bool $compact = false, ?string $class = null): string {
|
function aksrt_header_user_checkin_btn(int $user_id = 0, ?string $class = null): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
||||||
return '';
|
return '';
|
||||||
@@ -56,38 +55,39 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
|
|
||||||
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
||||||
if ($signed) {
|
if ($signed) {
|
||||||
$icon = '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>';
|
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
// 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
$class . ' is-done',
|
$class,
|
||||||
$icon . '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
|
. esc_html__('已签到', 'aksrt')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
'but block c-yellow aksrt-header-checkin is-done',
|
'but block c-yellow is-done',
|
||||||
'<i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt'))
|
'<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||||
|
. esc_html__('今日已签到', 'aksrt')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$classes = $class ?? ('but block c-yellow aksrt-header-checkin' . ($compact ? ' aksrt-header-checkin--compact' : ''));
|
|
||||||
|
|
||||||
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
||||||
// reference parity renders an <a> so the badge inherits the card font
|
// reference parity renders an <a> so the badge inherits the card font
|
||||||
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
return '<a class="' . esc_attr($classes) . '" href="javascript:;" data-signin-btn'
|
return '<a class="' . esc_attr($class) . '" href="javascript:;" data-signin-btn'
|
||||||
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||||
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
||||||
. '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
. '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
||||||
. '</a>';
|
. '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<button type="button" class="' . esc_attr($classes) . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
return '<a class="but block c-yellow" href="javascript:;" data-signin-btn'
|
||||||
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
|
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||||
. '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>'
|
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
|
||||||
. '</button>';
|
. '<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||||
|
. '<span data-signin-label>' . esc_html__('签到领取今日奖励', 'aksrt') . '</span>'
|
||||||
|
. '</a>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -118,42 +118,66 @@ add_action('wp_footer', 'aksrt_render_signin_captcha');
|
|||||||
|
|
||||||
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
||||||
/**
|
/**
|
||||||
* Mini wallet strip: credits + cash balance.
|
* Mini wallet tiles: cash balance + credits (reference parity).
|
||||||
|
*
|
||||||
|
* Each tile is gated by its payment switch; an all-off configuration
|
||||||
|
* renders nothing at all.
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0) {
|
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$credits = aksrt_user_points($user_id);
|
|
||||||
$currency = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_currency_name()
|
|
||||||
: __('积分', 'aksrt');
|
|
||||||
$wallet = aksrt_user_balance_decimal($user_id);
|
|
||||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
|
||||||
|
|
||||||
$html = '<a class="aksrt-header-wallet flex ac jsb" href="' . esc_url($center) . '">';
|
$balance_on = !empty(\AKSRT\Settings\Settings::get_payment_recharge_settings()['enabled']);
|
||||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-database" aria-hidden="true"></i> '
|
$points_on = \AKSRT\Settings\Settings::credits_enabled();
|
||||||
. esc_html($currency) . ' <b data-aksrt-points-value data-credits-value>' . esc_html(number_format_i18n($credits)) . '</b></span>';
|
if (!$balance_on && !$points_on) {
|
||||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-money-bill" aria-hidden="true"></i> '
|
return '';
|
||||||
. esc_html__('余额', 'aksrt') . ' <b data-aksrt-wallet-value>' . esc_html(number_format_i18n($wallet, 2)) . '</b></span>';
|
}
|
||||||
$html .= '</a>';
|
|
||||||
return $html;
|
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||||
|
$box = '';
|
||||||
|
$balance_icon = aksrt_icon_markup('zibsvg-money-color-2', 'em14');
|
||||||
|
$points_icon = aksrt_icon_markup('zibsvg-points-color', 'em14');
|
||||||
|
|
||||||
|
if ($balance_on) {
|
||||||
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
|
. '<div class="muted-2-color px12">' . esc_html__('余额', 'aksrt')
|
||||||
|
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||||
|
. '<div class="flex jsb"><span class="font-bold c-blue-2" data-aksrt-wallet-value data-aksrt-count-format="cut-count">'
|
||||||
|
. esc_html(aksrt_cut_count((float) aksrt_user_balance_decimal($user_id)))
|
||||||
|
. '</span>' . $balance_icon . '</div></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($points_on) {
|
||||||
|
$currency = \AKSRT\Settings\Settings::get_currency_name();
|
||||||
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
|
. '<div class="muted-2-color px12">' . esc_html($currency)
|
||||||
|
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||||
|
. '<div class="flex jsb"><span class="font-bold c-yellow" data-aksrt-points-value data-aksrt-count-format="cut-count">'
|
||||||
|
. esc_html(aksrt_cut_count(aksrt_user_points($user_id)))
|
||||||
|
. '</span>' . $points_icon . '</div></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_header_user_badges_row')) :
|
if (!function_exists('aksrt_header_user_badges_row')) :
|
||||||
/**
|
/**
|
||||||
* Identity / medal chips under profile.
|
* Interaction stats row under the wallet (reference parity).
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0 || !function_exists('aksrt_render_user_identity_chips')) {
|
if ($user_id <= 0 || !function_exists('aksrt_get_user_badges')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// Certification is displayed beside the name and level icon in the profile row.
|
$items = aksrt_get_user_badges($user_id);
|
||||||
|
if ($items === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
return '<div class="em09 author-tag mb10 mt6 flex jc">' . $items . '</div>';
|
||||||
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_header_payvip_bar')) :
|
if (!function_exists('aksrt_header_payvip_bar')) :
|
||||||
@@ -173,30 +197,33 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_all()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
||||||
if ($level > 0) {
|
if ($level > 0) {
|
||||||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||||||
$name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
$name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||||
|
// 到期徽章语义对齐参考主题 zib_get_user_vip_exp_date_text():只有真的拿到
|
||||||
|
// 到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;显式永久
|
||||||
|
// 会员显示「永久会员」;拿不到到期数据时参考主题返回 false,整枚徽章不渲染。
|
||||||
$exp_label = '';
|
$exp_label = '';
|
||||||
if (function_exists('aksrt_get_vip_info')) {
|
if (function_exists('aksrt_get_user_vip_state')) {
|
||||||
$info = aksrt_get_vip_info($user_id);
|
$state = aksrt_get_user_vip_state($user_id);
|
||||||
$expires = max(0, (int) ($info['expires'] ?? 0));
|
$expires = max(0, (int) ($state['expires'] ?? 0));
|
||||||
$exp_label = $expires > 0
|
if ($expires > 0) {
|
||||||
? sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires))
|
$exp_label = $expires <= time()
|
||||||
: (string) ($info['expires_label'] ?? '');
|
? __('会员已过期', 'aksrt')
|
||||||
|
: wp_date('Y年m月d日', $expires);
|
||||||
|
} elseif (!empty($state['is_permanent'])) {
|
||||||
|
$exp_label = __('永久会员', 'aksrt');
|
||||||
}
|
}
|
||||||
$html = '<div class="mt10 em09 aksrt-header-payvip is-vip">';
|
}
|
||||||
$html .= '<span class="radius4 payvip-icon btn-block text-center vipbg-v2 level-' . (int) $level . ' aksrt-header-payvip__active">';
|
$html = '<div class="mt10 em09" style="padding:2px;">';
|
||||||
$html .= '<span class="aksrt-header-payvip__level">';
|
$html .= '<span class="radius4 payvip-icon btn-block text-center">';
|
||||||
$html .= '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
$html .= function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id, 'em12 mr6', false) : '';
|
||||||
$html .= '<span>' . esc_html($name) . '</span>';
|
$html .= '<span>' . wp_kses_post($name) . '</span>';
|
||||||
$html .= '</span>';
|
|
||||||
if ($exp_label !== '') {
|
if ($exp_label !== '') {
|
||||||
$html .= '<span class="aksrt-header-payvip__exp">' . esc_html($exp_label) . '</span>';
|
$html .= '<span class="ml10 badg jb-yellow vip-expdate-tag">'
|
||||||
|
. '<i class="fa fas fa-clock mr3" aria-hidden="true"></i>'
|
||||||
|
. esc_html($exp_label) . '</span>';
|
||||||
}
|
}
|
||||||
$html .= '</span></div>';
|
$html .= '</span></div>';
|
||||||
return $html;
|
return $html;
|
||||||
@@ -205,11 +232,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match check-in CTA geometry (full width, 6px radius) — not top-nav navbar-cta chips.
|
// 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
|
||||||
return '<div class="mt10 em09 aksrt-header-payvip">'
|
return '<div class="mt10 em09" style="padding:2px;">'
|
||||||
. '<a class="pay-vip but block payvip-icon aksrt-header-payvip__cta" href="javascript:;" vip-level="1">'
|
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
|
||||||
. '<i class="fa fas fa-gem" aria-hidden="true"></i>'
|
. '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>'
|
. esc_html__('开通会员 尊享会员权益', 'aksrt')
|
||||||
. '</a></div>';
|
. '</a></div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('aksrt_brand_icon_map')) :
|
if (!function_exists('aksrt_brand_icon_map')) :
|
||||||
/**
|
/**
|
||||||
* Short key → FA5 class string (allowlisted).
|
* Short key → FA5 class string or built-in SVG sprite key (allowlisted).
|
||||||
*
|
*
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
|
|||||||
return $map;
|
return $map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FA5 Free has no bilibili/gitee brands — use solid stand-ins.
|
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
|
||||||
|
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
|
||||||
|
// a solid stand-in; every other key stays on FA5.
|
||||||
$map = [
|
$map = [
|
||||||
'github' => 'fa fab fa-github',
|
'github' => 'fa fab fa-github',
|
||||||
'google' => 'fa fab fa-google',
|
'google' => 'zibsvg-google-color',
|
||||||
'x' => 'fa fab fa-twitter',
|
'x' => 'fa fab fa-twitter',
|
||||||
'twitter' => 'fa fab fa-twitter',
|
'twitter' => 'fa fab fa-twitter',
|
||||||
'email' => 'fa fas fa-envelope',
|
'email' => 'fa fas fa-envelope',
|
||||||
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
|
|||||||
'instagram' => 'fa fab fa-instagram',
|
'instagram' => 'fa fab fa-instagram',
|
||||||
'web' => 'fa fas fa-globe',
|
'web' => 'fa fas fa-globe',
|
||||||
'globe' => 'fa fas fa-globe',
|
'globe' => 'fa fas fa-globe',
|
||||||
'gitee' => 'fa fas fa-code-branch',
|
'gitee' => 'zibsvg-gitee',
|
||||||
'baidu' => 'fa fas fa-paw',
|
'baidu' => 'fa fas fa-paw',
|
||||||
'alipay' => 'fa fab fa-alipay',
|
'alipay' => 'fa fab fa-alipay',
|
||||||
];
|
];
|
||||||
@@ -127,10 +129,10 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('aksrt_brand_icon_class')) :
|
if (!function_exists('aksrt_brand_icon_class')) :
|
||||||
/**
|
/**
|
||||||
* Resolve a short key or FA class string to an allowlisted FA5 class.
|
* Resolve a short key, FA class string or sprite key to its icon value.
|
||||||
*
|
*
|
||||||
* @param string $name Short key or full FA class string.
|
* @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
|
||||||
* @return string e.g. 'fa fab fa-github', or '' if unknown.
|
* @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
|
||||||
*/
|
*/
|
||||||
function aksrt_brand_icon_class(string $name): string {
|
function aksrt_brand_icon_class(string $name): string {
|
||||||
$raw = trim($name);
|
$raw = trim($name);
|
||||||
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Built-in SVG sprite and inline SVG values pass straight through; they are
|
||||||
|
// not FA class strings and must survive to the markup renderer untouched.
|
||||||
|
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
// Pass through already-valid full FA class strings (footer contact_more).
|
// Pass through already-valid full FA class strings (footer contact_more).
|
||||||
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
|
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
|
||||||
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
|
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
|
||||||
@@ -164,14 +172,21 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('aksrt_brand_icon')) :
|
if (!function_exists('aksrt_brand_icon')) :
|
||||||
/**
|
/**
|
||||||
* Escaped FA <i> markup for a brand/contact icon.
|
* Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
|
||||||
*
|
*
|
||||||
* @param string $name Short key or FA class string.
|
* @param string $name Short key, FA class string or zibsvg-/customsvg- value.
|
||||||
* @param array|string $extra_classes Optional extra classes.
|
* @param array|string $extra_classes Optional extra classes.
|
||||||
* @return string Safe HTML or empty string when unknown.
|
* @return string Safe HTML or empty string when unknown.
|
||||||
*/
|
*/
|
||||||
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
||||||
$class = aksrt_brand_icon_class($name);
|
$class = aksrt_brand_icon_class($name);
|
||||||
|
|
||||||
|
// Sprite and inline-SVG values carry their own element, so the shared icon
|
||||||
|
// renderer owns the markup (it emits <svg class="icon"><use …>).
|
||||||
|
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
|
||||||
|
return aksrt_icon_markup($class, $extra_classes);
|
||||||
|
}
|
||||||
|
|
||||||
if ($class === '') {
|
if ($class === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,14 +351,14 @@ if (!function_exists('aksrt_get_user_badges')) {
|
|||||||
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
||||||
|
|
||||||
$html = '';
|
$html = '';
|
||||||
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('post') . $post_n . '</a>';
|
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-file-alt" aria-hidden="true"></i>' . $post_n . '</a>';
|
||||||
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('comment') . $com_n . '</a>';
|
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-comment-dots" aria-hidden="true"></i>' . $com_n . '</a>';
|
||||||
if ($followed_n) {
|
if ($followed_n) {
|
||||||
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
||||||
} elseif ($like_n) {
|
} elseif ($like_n) {
|
||||||
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '">' . aksrt_renderer_svg_icon('like') . $like_n . '</span>';
|
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '"><i class="fa fas fa-thumbs-up" aria-hidden="true"></i>' . $like_n . '</span>';
|
||||||
}
|
}
|
||||||
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '">' . aksrt_renderer_svg_icon('hot') . $view_n . '</span>';
|
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '"><i class="fa fas fa-fire" aria-hidden="true"></i>' . $view_n . '</span>';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter the author-card count badges row.
|
* Filter the author-card count badges row.
|
||||||
|
|||||||
@@ -428,10 +428,7 @@ if (!function_exists('aksrt_submit_links_modal')) :
|
|||||||
$body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">'
|
$body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">'
|
||||||
. '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>';
|
. '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>';
|
||||||
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
||||||
$social = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
$body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
||||||
if ($social !== '') {
|
|
||||||
$body .= '<div class="social_loginbar">' . $social . '</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$body .= '</div>';
|
$body .= '</div>';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+45
-11
@@ -296,8 +296,8 @@ if (!function_exists('aksrt_get_download_box_meta')) {
|
|||||||
'view_count' => max(0, $view_count),
|
'view_count' => max(0, $view_count),
|
||||||
'virtual_sales' => $virtual_sales,
|
'virtual_sales' => $virtual_sales,
|
||||||
'sales_volume' => max(0, $sales_volume),
|
'sales_volume' => max(0, $sales_volume),
|
||||||
// legacy: virtual (pay_cuont) + real sales_volume; fall back to downloads when no sales yet.
|
// 展示销量 = 真实订单 + 后台「销量浮动」(pay_count)。
|
||||||
'sales_count' => $show_sales ? ($virtual_sales + max(0, $sales_volume, $download_count)) : 0,
|
'sales_count' => $show_sales ? aksrt_get_content_sales_count($p, $pay_meta) : 0,
|
||||||
'show_sales' => $show_sales,
|
'show_sales' => $show_sales,
|
||||||
'download_limit_over_price' => $over_price,
|
'download_limit_over_price' => $over_price,
|
||||||
'require_login' => !empty($pay_meta['require_login']),
|
'require_login' => !empty($pay_meta['require_login']),
|
||||||
@@ -352,6 +352,30 @@ if (!function_exists('aksrt_bump_content_sales_volume')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total sales shown to visitors: real paid orders plus the configured 销量浮动.
|
||||||
|
*
|
||||||
|
* pay_count is the only field behind the 后台「销量浮动」输入框(资源 metabox 与
|
||||||
|
* 快捷编辑都写这个键),此前没有任何读取方用它,导致设了不生效。所有展示销量
|
||||||
|
* 的地方都必须走这个口径,不要再各自拼 virtual_sales / sales_volume。
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $pay_meta 已取得的付费 meta,传空则按 $post_id 现取。
|
||||||
|
*/
|
||||||
|
if (!function_exists('aksrt_get_content_sales_count')) {
|
||||||
|
function aksrt_get_content_sales_count($post = null, array $pay_meta = []): int {
|
||||||
|
$post_id = $post instanceof \WP_Post ? (int) $post->ID : (int) $post;
|
||||||
|
if ($post_id <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ($pay_meta === []) {
|
||||||
|
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : [];
|
||||||
|
}
|
||||||
|
$real = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
||||||
|
$float = max(0, (int) ($pay_meta['pay_count'] ?? 0));
|
||||||
|
return max(0, $real + $float);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether guest cash purchase is allowed (legacy pay_no_logged_in + pay_limit=0).
|
* Whether guest cash purchase is allowed (legacy pay_no_logged_in + pay_limit=0).
|
||||||
*/
|
*/
|
||||||
@@ -1285,6 +1309,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level));
|
$level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level));
|
||||||
|
$level_name_html = (string) ($level_settings['name_html'] ?? $level_name);
|
||||||
$price = $vip_minor === 0
|
$price = $vip_minor === 0
|
||||||
? esc_html__('免费', 'aksrt')
|
? esc_html__('免费', 'aksrt')
|
||||||
: $mark . esc_html(aksrt_format_price($value));
|
: $mark . esc_html(aksrt_format_price($value));
|
||||||
@@ -1295,7 +1320,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
|||||||
? ' data-toggle="tooltip" title="' . esc_attr($title) . '"'
|
? ' data-toggle="tooltip" title="' . esc_attr($title) . '"'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . esc_html($level_name) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . wp_kses_post($level_name_html) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = implode('', $items);
|
$html = implode('', $items);
|
||||||
@@ -1348,8 +1373,7 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
|||||||
if ($doc === '') {
|
if ($doc === '') {
|
||||||
$doc = sprintf(__('此内容为%s,请付费后查看', 'aksrt'), $type_label);
|
$doc = sprintf(__('此内容为%s,请付费后查看', 'aksrt'), $type_label);
|
||||||
}
|
}
|
||||||
$sales = max(0, (int) ($meta['virtual_sales'] ?? 0));
|
$sales = aksrt_get_content_sales_count($post, $meta);
|
||||||
$sales += max(0, (int) get_post_meta($post->ID, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
|
||||||
$tag = $is_widget
|
$tag = $is_widget
|
||||||
? '<badge class="pay-tag abs-center">' . esc_html($type_label) . '</badge>'
|
? '<badge class="pay-tag abs-center">' . esc_html($type_label) . '</badge>'
|
||||||
: '<div class="pay-tag abs-center">' . esc_html($type_label) . '</div>';
|
: '<div class="pay-tag abs-center">' . esc_html($type_label) . '</div>';
|
||||||
@@ -1361,10 +1385,13 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
|||||||
$vip_action = $vip_level > 0
|
$vip_action = $vip_level > 0
|
||||||
? sprintf(__('升级%s', 'aksrt'), $level_name)
|
? sprintf(__('升级%s', 'aksrt'), $level_name)
|
||||||
: __('开通会员', 'aksrt');
|
: __('开通会员', 'aksrt');
|
||||||
|
$vip_action_html = $vip_level > 0
|
||||||
|
? __('升级', 'aksrt') . wp_kses_post((string) ($level_settings['name_html'] ?? $level_name))
|
||||||
|
: esc_html__('开通会员', 'aksrt');
|
||||||
$action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader';
|
$action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader';
|
||||||
$icon = aksrt_get_pay_vip_icon_html($level_settings);
|
$icon = aksrt_get_pay_vip_icon_html($level_settings);
|
||||||
$button = '<div class="badg c-yellow em09 mb6"><i class="fa fas fa-fw fa-info-circle mr6" aria-hidden="true"></i>' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '</div>';
|
$button = '<div class="badg c-yellow em09 mb6"><i class="fa fas fa-fw fa-info-circle mr6" aria-hidden="true"></i>' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '</div>';
|
||||||
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . esc_html($vip_action) . '</a>';
|
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . $vip_action_html . '</a>';
|
||||||
} elseif (!is_user_logged_in()) {
|
} elseif (!is_user_logged_in()) {
|
||||||
if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) {
|
if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) {
|
||||||
$button = '<button type="button" class="but jb-red padding-lg cashier-link' . $block_class . '" data-aksrt-pay-content="' . esc_attr((string) $post->ID) . '"><i class="fa fas fa-shopping-cart" aria-hidden="true"></i> ' . esc_html__('立即购买', 'aksrt') . '</button>';
|
$button = '<button type="button" class="but jb-red padding-lg cashier-link' . $block_class . '" data-aksrt-pay-content="' . esc_attr((string) $post->ID) . '"><i class="fa fas fa-shopping-cart" aria-hidden="true"></i> ' . esc_html__('立即购买', 'aksrt') . '</button>';
|
||||||
@@ -1379,7 +1406,8 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
|||||||
$price = aksrt_get_show_pay_price_html($meta, $priced);
|
$price = aksrt_get_show_pay_price_html($meta, $priced);
|
||||||
$vip = aksrt_get_posts_vip_price($meta);
|
$vip = aksrt_get_posts_vip_price($meta);
|
||||||
$attrs = aksrt_get_pay_attributes_html($post, 'pay-attr');
|
$attrs = aksrt_get_pay_attributes_html($post, 'pay-attr');
|
||||||
$count = $sales > 0 ? '<badge class="img-badge hot jb-blue px12">' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales)) . '</badge>' : '';
|
$sales_label = function_exists('aksrt_format_number') ? aksrt_format_number($sales) : number_format_i18n($sales);
|
||||||
|
$count = $sales > 0 ? '<badge class="img-badge hot jb-blue px12">' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales_label)) . '</badge>' : '';
|
||||||
$service = aksrt_get_pay_service_html('muted-2-color');
|
$service = aksrt_get_pay_service_html('muted-2-color');
|
||||||
$widget_service = $service !== '' ? '<div class="px12 muted-2-color">' . $service . '</div>' : '';
|
$widget_service = $service !== '' ? '<div class="px12 muted-2-color">' . $service . '</div>' : '';
|
||||||
$article_service = $service !== '' ? '<div class="px12 muted-2-color mt10 text-right">' . $service . '</div>' : '';
|
$article_service = $service !== '' ? '<div class="px12 muted-2-color mt10 text-right">' . $service . '</div>' : '';
|
||||||
@@ -1536,9 +1564,9 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
|||||||
$order_type = (int) ($order_types[$pay_type] ?? 1);
|
$order_type = (int) ($order_types[$pay_type] ?? 1);
|
||||||
$type_label = (string) ($type_labels[$pay_type] ?? __('付费内容', 'aksrt'));
|
$type_label = (string) ($type_labels[$pay_type] ?? __('付费内容', 'aksrt'));
|
||||||
$title = get_the_title($post);
|
$title = get_the_title($post);
|
||||||
$sales = max(0, (int) ($meta['virtual_sales'] ?? 0));
|
$sales = aksrt_get_content_sales_count($post, $meta);
|
||||||
$sales += max(0, (int) get_post_meta($post->ID, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
$sales_label = function_exists('aksrt_format_number') ? aksrt_format_number($sales) : number_format_i18n($sales);
|
||||||
$count = $sales > 0 ? '<badge class="img-badge hot jb-blue px12">' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales)) . '</badge>' : '';
|
$count = $sales > 0 ? '<badge class="img-badge hot jb-blue px12">' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales_label)) . '</badge>' : '';
|
||||||
$order = aksrt_get_current_content_paid_order($post);
|
$order = aksrt_get_current_content_paid_order($post);
|
||||||
$priced = aksrt_get_user_pay_price($post);
|
$priced = aksrt_get_user_pay_price($post);
|
||||||
$vip_level = max(0, min(2, (int) ($priced['vip_level'] ?? 0)));
|
$vip_level = max(0, min(2, (int) ($priced['vip_level'] ?? 0)));
|
||||||
@@ -1561,7 +1589,7 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
|||||||
}
|
}
|
||||||
$amount_text = $cash_symbol . aksrt_format_price($order_amount);
|
$amount_text = $cash_symbol . aksrt_format_price($order_amount);
|
||||||
}
|
}
|
||||||
$paid_info = '<div class="flex jsb"><span>' . esc_html__('流水号', 'aksrt') . '</span><span>' . esc_html($order_number) . '</span></div>';
|
$paid_info = '<div class="flex jsb"><span>' . esc_html__('订单号', 'aksrt') . '</span><span>' . esc_html($order_number) . '</span></div>';
|
||||||
if ($paid_at !== '') {
|
if ($paid_at !== '') {
|
||||||
$paid_info .= '<div class="flex jsb"><span>' . esc_html__('支付时间', 'aksrt') . '</span><span>' . esc_html($paid_at) . '</span></div>';
|
$paid_info .= '<div class="flex jsb"><span>' . esc_html__('支付时间', 'aksrt') . '</span><span>' . esc_html($paid_at) . '</span></div>';
|
||||||
}
|
}
|
||||||
@@ -1599,6 +1627,11 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
|||||||
$title_style = $count !== '' ? ' style="padding-right:48px"' : '';
|
$title_style = $count !== '' ? ' style="padding-right:48px"' : '';
|
||||||
$body_class = 'box-body relative' . ($pay_type === AKSRT_PAY_TYPE_VIDEO ? ' dplayer-featured' : '');
|
$body_class = 'box-body relative' . ($pay_type === AKSRT_PAY_TYPE_VIDEO ? ' dplayer-featured' : '');
|
||||||
|
|
||||||
|
// 付费后才展示的额外隐藏内容。整个函数只在有付费权限时才会走到这里,
|
||||||
|
// 因此无需再判断购买状态;HTML 由管理员在 metabox 里录入,已过 kses。
|
||||||
|
$pay_extra_hide = trim((string) ($meta['pay_extra_hide'] ?? ''));
|
||||||
|
$pay_extra_hide = $pay_extra_hide !== '' ? '<div class="pay-extra-hide">' . $pay_extra_hide . '</div>' : '';
|
||||||
|
|
||||||
return '<div class="pay-box zib-widget paid-box order-type-' . $order_type . '" id="posts-pay"' . $root_data . '>'
|
return '<div class="pay-box zib-widget paid-box order-type-' . $order_type . '" id="posts-pay"' . $root_data . '>'
|
||||||
. $header
|
. $header
|
||||||
. $paid_box
|
. $paid_box
|
||||||
@@ -1606,6 +1639,7 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
|||||||
. $count
|
. $count
|
||||||
. '<div' . $title_style . '><span class="badg c-red hollow badg-sm mr6">' . esc_html($type_label) . '</span><b>' . esc_html($title) . '</b></div>'
|
. '<div' . $title_style . '><span class="badg c-red hollow badg-sm mr6">' . esc_html($type_label) . '</span><b>' . esc_html($title) . '</b></div>'
|
||||||
. $download_box
|
. $download_box
|
||||||
|
. $pay_extra_hide
|
||||||
. $service
|
. $service
|
||||||
. '</div></div>';
|
. '</div></div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ if (!function_exists('aksrt_get_resource_meta')) {
|
|||||||
'download_count' => max(0, max($download_count, $legacy_download_count)),
|
'download_count' => max(0, max($download_count, $legacy_download_count)),
|
||||||
'view_count' => max(0, max($resource_view_count, $common_view_count)),
|
'view_count' => max(0, max($resource_view_count, $common_view_count)),
|
||||||
'virtual_sales' => $virtual_sales,
|
'virtual_sales' => $virtual_sales,
|
||||||
'sales_count' => $show_sales ? ($virtual_sales + max(0, max($download_count, $legacy_download_count))) : 0,
|
// 展示销量 = 真实订单 + 后台「销量浮动」(pay_count),与内容帖同一口径。
|
||||||
|
'sales_count' => $show_sales ? aksrt_get_content_sales_count($p, $pay_meta) : 0,
|
||||||
'show_sales' => $show_sales,
|
'show_sales' => $show_sales,
|
||||||
'require_login' => !empty($pay_meta['require_login']),
|
'require_login' => !empty($pay_meta['require_login']),
|
||||||
'require_captcha' => !empty($pay_meta['require_captcha']),
|
'require_captcha' => !empty($pay_meta['require_captcha']),
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ if (!function_exists('aksrt_get_posts_meta_items_html')) :
|
|||||||
? 'javascript:(typeof scrollTopTo===\'function\'?scrollTopTo(\'#comments\'):void(0));'
|
? 'javascript:(typeof scrollTopTo===\'function\'?scrollTopTo(\'#comments\'):void(0));'
|
||||||
: get_comments_link($post_id);
|
: get_comments_link($post_id);
|
||||||
$count = (int) get_comments_number($post_id);
|
$count = (int) get_comments_number($post_id);
|
||||||
|
$count_label = function_exists('aksrt_format_number') ? aksrt_format_number($count) : number_format_i18n($count);
|
||||||
$html .= '<span class="item meta-comm"><a rel="nofollow" href="' . esc_attr($href) . '" title="' . esc_attr__('去评论', 'aksrt') . '">'
|
$html .= '<span class="item meta-comm"><a rel="nofollow" href="' . esc_attr($href) . '" title="' . esc_attr__('去评论', 'aksrt') . '">'
|
||||||
. '<i class="fa fas fa-comment" aria-hidden="true"></i> ' . esc_html((string) $count) . '</a></span>';
|
. '<i class="fa fas fa-comment" aria-hidden="true"></i> ' . esc_html((string) $count_label) . '</a></span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : 0;
|
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : 0;
|
||||||
@@ -88,7 +89,8 @@ if (!function_exists('aksrt_get_posts_meta_items_html')) :
|
|||||||
}
|
}
|
||||||
if ($like_on && $post->post_type === 'post') {
|
if ($like_on && $post->post_type === 'post') {
|
||||||
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : 0;
|
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : 0;
|
||||||
$html .= '<span class="item meta-like"><i class="fa fas fa-heart" aria-hidden="true"></i> ' . esc_html((string) $likes) . '</span>';
|
$likes_label = function_exists('aksrt_format_number') ? aksrt_format_number($likes) : number_format_i18n($likes);
|
||||||
|
$html .= '<span class="item meta-like"><i class="fa fas fa-heart" aria-hidden="true"></i> ' . esc_html((string) $likes_label) . '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
|
|||||||
@@ -197,10 +197,11 @@ if (!function_exists('aksrt_single_render_actions')) :
|
|||||||
<div class="text-center post-actions">
|
<div class="text-center post-actions">
|
||||||
<?php
|
<?php
|
||||||
$like_icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-like') : '';
|
$like_icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-like') : '';
|
||||||
|
$like_label = function_exists('aksrt_format_number') ? aksrt_format_number($like_count) : number_format_i18n($like_count);
|
||||||
if (!empty($settings['like_enabled'])) : ?>
|
if (!empty($settings['like_enabled'])) : ?>
|
||||||
<button type="button" class="action action-like<?php echo $is_liked ? ' actived' : ''; ?>" id="action-like" data-post-id="<?php echo esc_attr((string) $post_id); ?>" data-initial-active="<?php echo $is_liked ? '1' : '0'; ?>">
|
<button type="button" class="action action-like<?php echo $is_liked ? ' actived' : ''; ?>" id="action-like" data-post-id="<?php echo esc_attr((string) $post_id); ?>" data-initial-active="<?php echo $is_liked ? '1' : '0'; ?>">
|
||||||
<?php echo $like_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
<?php echo $like_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
||||||
<text><?php esc_html_e('点赞', 'aksrt'); ?></text><count data-stat-count="likes" data-base-count="<?php echo esc_attr((string) $like_count); ?>"><?php echo esc_html((string) $like_count); ?></count>
|
<text><?php esc_html_e('点赞', 'aksrt'); ?></text><count data-stat-count="likes" data-base-count="<?php echo esc_attr((string) $like_count); ?>"><?php echo esc_html((string) $like_label); ?></count>
|
||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -221,10 +222,11 @@ if (!function_exists('aksrt_single_render_actions')) :
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
$favorite_icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-favorite') : '';
|
$favorite_icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-favorite') : '';
|
||||||
|
$favorite_label = function_exists('aksrt_format_number') ? aksrt_format_number($favorite_count) : number_format_i18n($favorite_count);
|
||||||
if (!empty($settings['favorite_enabled'])) : ?>
|
if (!empty($settings['favorite_enabled'])) : ?>
|
||||||
<button type="button" class="action action-favorite<?php echo $is_favorited ? ' actived' : ''; ?>" id="action-favorite" data-post-id="<?php echo esc_attr((string) $post_id); ?>" data-initial-active="<?php echo $is_favorited ? '1' : '0'; ?>">
|
<button type="button" class="action action-favorite<?php echo $is_favorited ? ' actived' : ''; ?>" id="action-favorite" data-post-id="<?php echo esc_attr((string) $post_id); ?>" data-initial-active="<?php echo $is_favorited ? '1' : '0'; ?>">
|
||||||
<?php echo $favorite_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
<?php echo $favorite_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
||||||
<text><?php esc_html_e('收藏', 'aksrt'); ?></text><count data-stat-count="favorites" data-base-count="<?php echo esc_attr((string) $favorite_count); ?>"><?php echo esc_html((string) $favorite_count); ?></count>
|
<text><?php esc_html_e('收藏', 'aksrt'); ?></text><count data-stat-count="favorites" data-base-count="<?php echo esc_attr((string) $favorite_count); ?>"><?php echo esc_html((string) $favorite_label); ?></count>
|
||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,11 +285,12 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
|||||||
}
|
}
|
||||||
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
||||||
|
|
||||||
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg');
|
// 会员/禁封徽章不再挂头像角标,改由名字行的会员位出图(aksrt_get_user_name)。
|
||||||
|
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg', true, false);
|
||||||
$name_html = aksrt_get_user_name([
|
$name_html = aksrt_get_user_name([
|
||||||
'id' => $author_id,
|
'id' => $author_id,
|
||||||
'class' => 'flex1 flex ac',
|
'class' => 'flex1 flex ac',
|
||||||
'vip' => false,
|
'vip' => true,
|
||||||
'medal' => true,
|
'medal' => true,
|
||||||
'auth' => true,
|
'auth' => true,
|
||||||
'level' => true,
|
'level' => true,
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ endif;
|
|||||||
* or Chinese keyword heuristics — that mangles intentional HTML.
|
* or Chinese keyword heuristics — that mangles intentional HTML.
|
||||||
*
|
*
|
||||||
* Rules:
|
* Rules:
|
||||||
* - Sanitize with a small allowlist.
|
* - Sanitize with WordPress post-safe HTML.
|
||||||
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
||||||
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
||||||
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
||||||
@@ -497,21 +497,7 @@ if (!function_exists('aksrt_format_vip_equity_html')) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowed = [
|
$clean = trim(wp_kses_post($raw));
|
||||||
'li' => ['class' => true],
|
|
||||||
'ul' => ['class' => true],
|
|
||||||
'ol' => ['class' => true],
|
|
||||||
'strong' => [],
|
|
||||||
'b' => [],
|
|
||||||
'em' => [],
|
|
||||||
'i' => ['class' => true],
|
|
||||||
'span' => ['class' => true],
|
|
||||||
'br' => [],
|
|
||||||
'p' => ['class' => true],
|
|
||||||
'div' => ['class' => true],
|
|
||||||
'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true],
|
|
||||||
];
|
|
||||||
$clean = trim(wp_kses($raw, $allowed));
|
|
||||||
if ($clean === '') {
|
if ($clean === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ if (!function_exists('aksrt_get_comment_like_html')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$like_count = function_exists('aksrt_get_comment_likes') ? (int) aksrt_get_comment_likes($comment_id) : 0;
|
$like_count = function_exists('aksrt_get_comment_likes') ? (int) aksrt_get_comment_likes($comment_id) : 0;
|
||||||
|
$like_label = function_exists('aksrt_format_number') ? aksrt_format_number($like_count) : number_format_i18n($like_count);
|
||||||
$is_liked = function_exists('aksrt_is_comment_liked') && aksrt_is_comment_liked($comment_id);
|
$is_liked = function_exists('aksrt_is_comment_liked') && aksrt_is_comment_liked($comment_id);
|
||||||
$classes = 'action action-comment-like muted-2-color flex0 ml10' . ($is_liked ? ' actived' : '');
|
$classes = 'action action-comment-like muted-2-color flex0 ml10' . ($is_liked ? ' actived' : '');
|
||||||
$icon = function_exists('aksrt_get_comment_icon') ? aksrt_get_comment_icon('like', 'icon mr3') : '';
|
$icon = function_exists('aksrt_get_comment_icon') ? aksrt_get_comment_icon('like', 'icon mr3') : '';
|
||||||
@@ -38,7 +39,7 @@ if (!function_exists('aksrt_get_comment_like_html')) :
|
|||||||
$is_liked ? '1' : '0',
|
$is_liked ? '1' : '0',
|
||||||
$is_liked ? 'true' : 'false',
|
$is_liked ? 'true' : 'false',
|
||||||
$icon, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup.
|
$icon, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup.
|
||||||
esc_html((string) $like_count)
|
esc_html((string) $like_label)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
if ($user_id > 0) {
|
if ($user_id > 0) {
|
||||||
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
||||||
$display_name = $user->display_name ?: $user->user_login;
|
$display_name = $user->display_name ?: $user->user_login;
|
||||||
$avatar = aksrt_get_avatar_html($user_id, 44, ['alt' => $display_name]);
|
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img', true, false);
|
||||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||||
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
||||||
// Level and certification icons ride inline to the right of the display name.
|
// Level and certification icons ride inline to the right of the display name.
|
||||||
@@ -174,10 +174,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
|
|
||||||
$msg_icon = '';
|
$msg_icon = '';
|
||||||
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
||||||
$msg_icon = '<a class="abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
$msg_icon = '<a class="msg-news-icon abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
||||||
$msg_icon .= '<i class="fa fas fa-bell" aria-hidden="true"></i>';
|
$msg_icon .= '<span class="toggle-radius msg-icon"><i class="fa fas fa-bell" aria-hidden="true"></i></span>';
|
||||||
if ($unread > 0) {
|
if ($unread > 0) {
|
||||||
$msg_icon .= '<span class="aksrt-notification-badge">' . esc_html((string) min(99, $unread)) . '</span>';
|
$msg_icon .= '<badge class="top">' . esc_html((string) min(99, $unread)) . '</badge>';
|
||||||
}
|
}
|
||||||
$msg_icon .= '</a>';
|
$msg_icon .= '</a>';
|
||||||
}
|
}
|
||||||
@@ -224,6 +224,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
||||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
||||||
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
||||||
|
// legacy: 商城中心 → 支付中心(AKSRT 支付中心后台菜单)。
|
||||||
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('admin.php?page=aksrt_pay')) . '">';
|
||||||
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-credit-card" aria-hidden="true"></i></div>';
|
||||||
|
$href .= '<div class="c-yellow">' . esc_html__('支付中心', 'aksrt') . '</div></a>';
|
||||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
||||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
||||||
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
||||||
@@ -234,7 +238,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$con .= $avatar;
|
$con .= $avatar;
|
||||||
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
||||||
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
||||||
. '<span class="display-name text-ellipsis">' . esc_html($display_name) . '</span>' . $vip_badge . $medal_badge . $certification_badge . $level_badge
|
. '<name class="flex ac flex1">'
|
||||||
|
. '<a class="display-name text-ellipsis" href="' . esc_url(function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id)) . '">' . esc_html($display_name) . '</a>'
|
||||||
|
. $vip_badge . $medal_badge . $certification_badge . $level_badge
|
||||||
|
. '</name>'
|
||||||
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
||||||
$con .= '</div>';
|
$con .= '</div>';
|
||||||
$con .= $msg_icon;
|
$con .= $msg_icon;
|
||||||
@@ -264,17 +271,22 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
||||||
. esc_html__('注册', 'aksrt') . '</div></a>';
|
. esc_html__('注册', 'aksrt') . '</div></a>';
|
||||||
}
|
}
|
||||||
$href .= '<a rel="nofollow" href="' . esc_url(wp_lostpassword_url(home_url('/'))) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
// legacy: 找回密码 → 统一登录页 resetpassword 标签(带回跳)。
|
||||||
|
$reset_url = function_exists('aksrt_get_sign_page_url')
|
||||||
|
? aksrt_get_sign_page_url('resetpassword', [
|
||||||
|
'redirect_to' => remove_query_arg(
|
||||||
|
'redirect_to',
|
||||||
|
home_url(isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '/')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: wp_lostpassword_url(home_url('/'));
|
||||||
|
$href .= '<a target="_blank" rel="nofollow" href="' . esc_url($reset_url) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
||||||
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
||||||
$href .= '</div>';
|
$href .= '</div>';
|
||||||
|
|
||||||
$con = '<div class="text-center">' . $href . '</div>';
|
$con = '<div class="text-center">' . $href . '</div>';
|
||||||
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
||||||
$oauth_html = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
|
$con .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
|
||||||
if ($oauth_html !== '') {
|
|
||||||
$con .= '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html__('快速登录', 'aksrt') . '</p>';
|
|
||||||
$con .= '<div class="social_loginbar">' . $oauth_html . '</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +299,12 @@ if (!function_exists('aksrt_menu_button')) :
|
|||||||
/**
|
/**
|
||||||
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
||||||
*
|
*
|
||||||
|
* The reference splits the cluster into three floated .navbar-form
|
||||||
|
* groups (publish/VIP, theme + language, account row). Each group
|
||||||
|
* keeps its own .navbar-form padding, which is what produces the
|
||||||
|
* reference inter-cluster spacing, so the groups are emitted
|
||||||
|
* separately instead of being merged into one flex container.
|
||||||
|
*
|
||||||
* @param array<string,mixed> $ctx
|
* @param array<string,mixed> $ctx
|
||||||
*/
|
*/
|
||||||
function aksrt_menu_button(array $ctx): void {
|
function aksrt_menu_button(array $ctx): void {
|
||||||
@@ -355,14 +373,24 @@ if (!function_exists('aksrt_menu_button')) :
|
|||||||
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
||||||
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||||
. $notification_but . $sign_but . $search_but . '</ul>';
|
. $notification_but . $sign_but . $search_but . '</ul>';
|
||||||
$button = '<div class="aksrt-navbar-actions navbar-form navbar-right navbar-but">'
|
|
||||||
. $actions . $utility_buttons . $account . '</div>';
|
// Reference cluster split: publish/VIP, theme + language, account row.
|
||||||
|
$button = '';
|
||||||
|
if ($actions !== '') {
|
||||||
|
$button = '<div class="navbar-form navbar-right navbar-but">' . $actions . '</div>';
|
||||||
|
}
|
||||||
|
if ($utility_buttons !== '') {
|
||||||
|
$button .= '<div class="navbar-form navbar-right flex ac">' . $utility_buttons . '</div>';
|
||||||
|
}
|
||||||
|
$right_but = '<div class="navbar-form navbar-right'
|
||||||
|
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||||
|
. $account . '</div>';
|
||||||
|
|
||||||
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
||||||
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
||||||
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
||||||
|
|
||||||
$html = $fold . $button;
|
$html = $fold . $button . $right_but;
|
||||||
|
|
||||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ if (!function_exists('aksrt_renderer_meta')) {
|
|||||||
|
|
||||||
$right = '';
|
$right = '';
|
||||||
if (!empty($data['comments_open']) && empty($cfg['close_comments'])) {
|
if (!empty($data['comments_open']) && empty($cfg['close_comments'])) {
|
||||||
$right .= '<item class="meta-comm"><a rel="nofollow" data-toggle="tooltip" title="' . esc_attr__('去评论', 'aksrt') . '" href="' . esc_url((string) $data['comments_link']) . '">' . aksrt_renderer_svg_icon('comment') . esc_html((string) $data['comments']) . '</a></item>';
|
$right .= '<item class="meta-comm"><a rel="nofollow" data-toggle="tooltip" title="' . esc_attr__('去评论', 'aksrt') . '" href="' . esc_url((string) $data['comments_link']) . '">' . aksrt_renderer_svg_icon('comment') . esc_html(aksrt_renderer_meta_number((int) $data['comments'])) . '</a></item>';
|
||||||
}
|
}
|
||||||
$right .= '<item class="meta-view">' . aksrt_renderer_svg_icon('view') . esc_html(aksrt_renderer_meta_number((int) $data['views'])) . '</item>';
|
$right .= '<item class="meta-view">' . aksrt_renderer_svg_icon('view') . esc_html(aksrt_renderer_meta_number((int) $data['views'])) . '</item>';
|
||||||
if (!empty($cfg['show_like']) && (string) ($data['post_type'] ?? '') === 'post') {
|
if (!empty($cfg['show_like']) && (string) ($data['post_type'] ?? '') === 'post') {
|
||||||
|
|||||||
@@ -422,6 +422,9 @@ if (!function_exists('aksrt_render_footer_tabbar_single_extend')) :
|
|||||||
$is_liked = function_exists('aksrt_is_post_liked') ? aksrt_is_post_liked($post_id) : false;
|
$is_liked = function_exists('aksrt_is_post_liked') ? aksrt_is_post_liked($post_id) : false;
|
||||||
$is_favorited = function_exists('aksrt_is_post_favorited') ? aksrt_is_post_favorited($post_id) : false;
|
$is_favorited = function_exists('aksrt_is_post_favorited') ? aksrt_is_post_favorited($post_id) : false;
|
||||||
$comment_count = (int) get_comments_number($post_id);
|
$comment_count = (int) get_comments_number($post_id);
|
||||||
|
$like_label = function_exists('aksrt_format_number') ? aksrt_format_number($like_count) : number_format_i18n($like_count);
|
||||||
|
$favorite_label = function_exists('aksrt_format_number') ? aksrt_format_number($favorite_count) : number_format_i18n($favorite_count);
|
||||||
|
$comment_label = function_exists('aksrt_format_number') ? aksrt_format_number($comment_count) : number_format_i18n($comment_count);
|
||||||
|
|
||||||
if ($like_on) :
|
if ($like_on) :
|
||||||
$like_tabbar_icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-like') : '';
|
$like_tabbar_icon = function_exists('aksrt_icon_markup') ? aksrt_icon_markup('zibsvg-like') : '';
|
||||||
@@ -433,7 +436,7 @@ if (!function_exists('aksrt_render_footer_tabbar_single_extend')) :
|
|||||||
data-initial-active="<?php echo $is_liked ? '1' : '0'; ?>"
|
data-initial-active="<?php echo $is_liked ? '1' : '0'; ?>"
|
||||||
aria-label="<?php esc_attr_e('点赞', 'aksrt'); ?>">
|
aria-label="<?php esc_attr_e('点赞', 'aksrt'); ?>">
|
||||||
<?php echo $like_tabbar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
<?php echo $like_tabbar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
||||||
<?php if ($show_text) : ?><count data-stat-count="likes" data-base-count="<?php echo esc_attr((string) $like_count); ?>"><?php echo esc_html((string) $like_count); ?></count><?php endif; ?>
|
<?php if ($show_text) : ?><count data-stat-count="likes" data-base-count="<?php echo esc_attr((string) $like_count); ?>"><?php echo esc_html((string) $like_label); ?></count><?php endif; ?>
|
||||||
</button>
|
</button>
|
||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
@@ -448,7 +451,7 @@ if (!function_exists('aksrt_render_footer_tabbar_single_extend')) :
|
|||||||
data-initial-active="<?php echo $is_favorited ? '1' : '0'; ?>"
|
data-initial-active="<?php echo $is_favorited ? '1' : '0'; ?>"
|
||||||
aria-label="<?php esc_attr_e('收藏', 'aksrt'); ?>">
|
aria-label="<?php esc_attr_e('收藏', 'aksrt'); ?>">
|
||||||
<?php echo $favorite_tabbar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
<?php echo $favorite_tabbar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed sprite markup. ?>
|
||||||
<?php if ($show_text) : ?><count data-stat-count="favorites" data-base-count="<?php echo esc_attr((string) $favorite_count); ?>"><?php echo esc_html((string) $favorite_count); ?></count><?php endif; ?>
|
<?php if ($show_text) : ?><count data-stat-count="favorites" data-base-count="<?php echo esc_attr((string) $favorite_count); ?>"><?php echo esc_html((string) $favorite_label); ?></count><?php endif; ?>
|
||||||
</button>
|
</button>
|
||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
@@ -464,7 +467,7 @@ if (!function_exists('aksrt_render_footer_tabbar_single_extend')) :
|
|||||||
data-type="comment"
|
data-type="comment"
|
||||||
aria-label="<?php esc_attr_e('评论', 'aksrt'); ?>">
|
aria-label="<?php esc_attr_e('评论', 'aksrt'); ?>">
|
||||||
<i class="fa fas fa-comment" aria-hidden="true"></i>
|
<i class="fa fas fa-comment" aria-hidden="true"></i>
|
||||||
<?php if ($show_text) : ?><span><?php echo esc_html((string) $comment_count); ?></span><?php endif; ?>
|
<?php if ($show_text) : ?><span><?php echo esc_html((string) $comment_label); ?></span><?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
/**
|
/**
|
||||||
* Affiliate poster preview modal.
|
* Affiliate poster preview modal.
|
||||||
*
|
*
|
||||||
* The 推广海报 button lives in the revenue detail link row; the QR code is no
|
* 1:1 replica of the reference theme's poster-share modal (zibll js/poster-share.js
|
||||||
* longer rendered on its own — it exists only inside the generated poster.
|
* + 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
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
@@ -15,14 +19,18 @@ if (!empty($GLOBALS['aksrt_affiliate_poster_modal_rendered'])) {
|
|||||||
}
|
}
|
||||||
$GLOBALS['aksrt_affiliate_poster_modal_rendered'] = true;
|
$GLOBALS['aksrt_affiliate_poster_modal_rendered'] = true;
|
||||||
?>
|
?>
|
||||||
<div class="uc-modal" id="aksrt-affiliate-poster-modal" hidden data-aksrt-affiliate-poster-modal>
|
<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="uc-modal__overlay" data-affiliate-poster-close></div>
|
<div class="modal-dialog poster-share hover-show" role="document">
|
||||||
<div class="uc-modal__dialog" role="dialog" aria-modal="true">
|
<div class="modal-content" style="height:220px;overflow:hidden">
|
||||||
<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>
|
<div class="poster-imgbox hover-show" data-affiliate-poster-stage></div>
|
||||||
<h3 class="uc-modal__title"><?php esc_html_e('推广海报', 'aksrt'); ?></h3>
|
<div class="flex jc loading-mask absolute main-bg radius8" data-affiliate-poster-loading>
|
||||||
<div class="uc-affiliate-poster" data-affiliate-poster-stage>
|
<div class="loading zts fa-2x"></div>
|
||||||
<p class="muted-2-color em09"><?php esc_html_e('正在生成海报…', 'aksrt'); ?></p>
|
<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>
|
</div>
|
||||||
<button type="button" class="but jb-blue btn-block mt10" data-affiliate-poster-download hidden><?php esc_html_e('保存海报', 'aksrt'); ?></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +102,7 @@ $currency_labels = [
|
|||||||
$status = (string) ($item['status'] ?? '') === 'pending' ? 'pending' : 'settled';
|
$status = (string) ($item['status'] ?? '') === 'pending' ? 'pending' : 'settled';
|
||||||
$amount = $format($item['amount'] ?? 0, $unit);
|
$amount = $format($item['amount'] ?? 0, $unit);
|
||||||
$order_number = trim((string) ($item['order_number'] ?? ''));
|
$order_number = trim((string) ($item['order_number'] ?? ''));
|
||||||
$order_number = $order_number !== '' ? $order_number : __('流水号未知', 'aksrt');
|
$order_number = $order_number !== '' ? $order_number : __('订单号未知', 'aksrt');
|
||||||
$post = get_post((int) ($item['post_id'] ?? 0));
|
$post = get_post((int) ($item['post_id'] ?? 0));
|
||||||
$source = $post instanceof WP_Post && $post->post_type === 'post'
|
$source = $post instanceof WP_Post && $post->post_type === 'post'
|
||||||
? get_the_title($post)
|
? get_the_title($post)
|
||||||
|
|||||||
@@ -121,7 +121,26 @@ $status_options = [
|
|||||||
<button class="btn-outline" type="button" data-aksrt-order-detail="<?php echo esc_attr($number); ?>"><?php esc_html_e('订单详情', 'aksrt'); ?></button>
|
<button class="btn-outline" type="button" data-aksrt-order-detail="<?php echo esc_attr($number); ?>"><?php esc_html_e('订单详情', 'aksrt'); ?></button>
|
||||||
<?php if (!empty($actions['can_cancel'])) : ?><button class="btn-ghost" type="button" data-aksrt-order-cancel="<?php echo esc_attr($number); ?>"><?php esc_html_e('关闭订单', 'aksrt'); ?></button><?php endif; ?>
|
<?php if (!empty($actions['can_cancel'])) : ?><button class="btn-ghost" type="button" data-aksrt-order-cancel="<?php echo esc_attr($number); ?>"><?php esc_html_e('关闭订单', 'aksrt'); ?></button><?php endif; ?>
|
||||||
<?php if (!empty($actions['can_pay'])) : ?><button class="btn-primary" type="button" data-aksrt-order-pay="<?php echo esc_attr($number); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('继续支付', 'aksrt'); ?></button><?php endif; ?>
|
<?php if (!empty($actions['can_pay'])) : ?><button class="btn-primary" type="button" data-aksrt-order-pay="<?php echo esc_attr($number); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('继续支付', 'aksrt'); ?></button><?php endif; ?>
|
||||||
<?php if (!empty($actions['can_after_sale'])) : ?><button class="btn-outline" type="button" data-aksrt-order-detail="<?php echo esc_attr($number); ?>"><?php esc_html_e('申请售后', 'aksrt'); ?></button><?php endif; ?>
|
<?php
|
||||||
|
$after_sale = is_array($order['after_sale'] ?? null) ? $order['after_sale'] : [];
|
||||||
|
// 售后弹窗载荷随按钮渲染(同举报链路 data-aksrt-report):点击即同步弹窗。
|
||||||
|
$after_sale_payload = wp_json_encode([
|
||||||
|
'order_number' => $number,
|
||||||
|
'product_name' => (string) ($order['product_name'] ?? ''),
|
||||||
|
'amount' => (string) ($order['amount'] ?? '0'),
|
||||||
|
'is_points' => !empty($order['is_points']),
|
||||||
|
'currency_symbol' => (string) ($order['currency_symbol'] ?? ''),
|
||||||
|
'status_label' => (string) ($order['status_label'] ?? ''),
|
||||||
|
'after_sale' => $after_sale,
|
||||||
|
]);
|
||||||
|
?>
|
||||||
|
<?php if (!empty($after_sale['enabled'])) : ?>
|
||||||
|
<?php if (!empty($after_sale['has_application'])) : ?>
|
||||||
|
<button class="btn-outline" type="button" data-aksrt-after-sale-detail="<?php echo esc_attr($after_sale_payload); ?>"><?php esc_html_e('售后详细', 'aksrt'); ?></button>
|
||||||
|
<?php else : ?>
|
||||||
|
<button class="btn-outline" type="button" data-aksrt-after-sale-apply="<?php echo esc_attr($after_sale_payload); ?>"><?php esc_html_e('申请售后', 'aksrt'); ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -131,4 +150,13 @@ $status_options = [
|
|||||||
<div class="aksrt-orders-page__empty" data-aksrt-order-list><i class="fa fas fa-file-alt" aria-hidden="true"></i><h3><?php esc_html_e('还没有购买记录', 'aksrt'); ?></h3><p><?php esc_html_e('购买内容或开通权益后,订单会出现在这里。', 'aksrt'); ?></p></div>
|
<div class="aksrt-orders-page__empty" data-aksrt-order-list><i class="fa fas fa-file-alt" aria-hidden="true"></i><h3><?php esc_html_e('还没有购买记录', 'aksrt'); ?></h3><p><?php esc_html_e('购买内容或开通权益后,订单会出现在这里。', 'aksrt'); ?></p></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 售后人机验证挂载点(申请 / 撤销):场景未启用时 aksrt_render_captcha 无输出,
|
||||||
|
// 前端 order-after-sale.js 以挂载点存在与否决定是否先过验证。
|
||||||
|
if (function_exists('aksrt_render_captcha')) {
|
||||||
|
aksrt_render_captcha('after_sale_apply');
|
||||||
|
aksrt_render_captcha('after_sale_cancel');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +121,6 @@ $status_labels = [
|
|||||||
$pool_amount = $format_amount($commission_stats['pool'] ?? 0);
|
$pool_amount = $format_amount($commission_stats['pool'] ?? 0);
|
||||||
$total_amount = $format_amount($commission_stats['total'] ?? 0);
|
$total_amount = $format_amount($commission_stats['total'] ?? 0);
|
||||||
$withdrawn_amount = $format_amount($commission_stats['withdrawn'] ?? 0);
|
$withdrawn_amount = $format_amount($commission_stats['withdrawn'] ?? 0);
|
||||||
$debt_amount = $format_amount($commission_stats['debt'] ?? 0);
|
|
||||||
$reward_panes = ['detail'];
|
$reward_panes = ['detail'];
|
||||||
if ($affiliate_on) {
|
if ($affiliate_on) {
|
||||||
$reward_panes[] = 'orders';
|
$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">
|
<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($total_amount)); ?></span>
|
||||||
<span><?php printf(esc_html__('已提现 ¥%s', 'aksrt'), esc_html($withdrawn_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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($withdraw_on) : ?>
|
<?php if ($withdraw_on) : ?>
|
||||||
@@ -178,7 +176,7 @@ $pane_is_active = static function (string $tab) use ($active_reward_tab): bool {
|
|||||||
</div>
|
</div>
|
||||||
<?php if ($affiliate_on) : ?>
|
<?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"><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>
|
<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; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ require __DIR__ . '/sidebar-vip.php';
|
|||||||
require __DIR__ . '/sidebar-commerce.php';
|
require __DIR__ . '/sidebar-commerce.php';
|
||||||
require __DIR__ . '/sidebar-creator.php';
|
require __DIR__ . '/sidebar-creator.php';
|
||||||
require __DIR__ . '/sidebar-nav.php';
|
require __DIR__ . '/sidebar-nav.php';
|
||||||
|
$drawer_trigger_icon = aksrt_icon_markup('zibsvg-menu');
|
||||||
?>
|
?>
|
||||||
<button type="button" class="uc-sidebar__drawer-trigger" id="uc-drawer-trigger" aria-label="<?php esc_attr_e('打开导航', 'aksrt'); ?>">
|
<button type="button" class="uc-sidebar__drawer-trigger" id="uc-drawer-trigger" aria-label="<?php esc_attr_e('打开导航', 'aksrt'); ?>">
|
||||||
<i class="fa fas fa-th-large" aria-hidden="true"></i>
|
<?php echo $drawer_trigger_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</button>
|
</button>
|
||||||
@@ -26,10 +26,14 @@ foreach ((array) ($oauth_data['bindings'] ?? []) as $binding) {
|
|||||||
if (!$key) {
|
if (!$key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Reference parity: the provider type (clogin_ prefix stripped) drives the
|
||||||
|
// .social-login-item brand colour, matching zibll's binding list.
|
||||||
|
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||||
$binding_rows[$key] = [
|
$binding_rows[$key] = [
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||||
'icon' => $binding['icon'] ?? '',
|
'icon' => $binding['icon'] ?? '',
|
||||||
|
'icon_type' => $icon_type,
|
||||||
'is_bound' => true,
|
'is_bound' => true,
|
||||||
'url' => '',
|
'url' => '',
|
||||||
];
|
];
|
||||||
@@ -39,10 +43,12 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
|||||||
if (!$key) {
|
if (!$key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||||
$binding_rows[$key] = [
|
$binding_rows[$key] = [
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||||
'icon' => $binding['icon'] ?? '',
|
'icon' => $binding['icon'] ?? '',
|
||||||
|
'icon_type' => $icon_type,
|
||||||
'is_bound' => false,
|
'is_bound' => false,
|
||||||
'url' => $binding['start_url'] ?? $binding['url'] ?? '',
|
'url' => $binding['start_url'] ?? $binding['url'] ?? '',
|
||||||
];
|
];
|
||||||
@@ -64,7 +70,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
|||||||
<?php foreach ($binding_rows as $binding) : ?>
|
<?php foreach ($binding_rows as $binding) : ?>
|
||||||
<?php if (!empty($binding['is_bound'])) : ?>
|
<?php if (!empty($binding['is_bound'])) : ?>
|
||||||
<div class="aksrt-account-row binding-item">
|
<div class="aksrt-account-row binding-item">
|
||||||
<span class="binding-item__icon"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
<span class="social-login-item circular <?php echo esc_attr($binding['icon_type']); ?>"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||||
<span class="binding-status">
|
<span class="binding-status">
|
||||||
<i class="fa fas fa-check-circle badge__icon" aria-hidden="true"></i>
|
<i class="fa fas fa-check-circle badge__icon" aria-hidden="true"></i>
|
||||||
@@ -76,7 +82,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
|||||||
</div>
|
</div>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<a href="<?php echo esc_url($binding['url']); ?>" class="aksrt-account-row binding-item binding-item--linkable" target="_self" data-provider="<?php echo esc_attr($binding['key']); ?>" data-bind-start-url="<?php echo esc_url($binding['url']); ?>">
|
<a href="<?php echo esc_url($binding['url']); ?>" class="aksrt-account-row binding-item binding-item--linkable" target="_self" data-provider="<?php echo esc_attr($binding['key']); ?>" data-bind-start-url="<?php echo esc_url($binding['url']); ?>">
|
||||||
<span class="binding-item__icon"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
<span class="social-login-item circular <?php echo esc_attr($binding['icon_type']); ?>"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||||
<span class="binding-status binding-status--idle"><?php esc_html_e('未绑定', 'aksrt'); ?></span>
|
<span class="binding-status binding-status--idle"><?php esc_html_e('未绑定', 'aksrt'); ?></span>
|
||||||
<span class="but hollow c-blue aksrt-account-row__action"><?php esc_html_e('绑定', 'aksrt'); ?></span>
|
<span class="but hollow c-blue aksrt-account-row__action"><?php esc_html_e('绑定', 'aksrt'); ?></span>
|
||||||
|
|||||||
@@ -20,23 +20,26 @@ try {
|
|||||||
$wallet = '0.00';
|
$wallet = '0.00';
|
||||||
}
|
}
|
||||||
$points = max(0, (int) ($credit_balance ?? 0));
|
$points = max(0, (int) ($credit_balance ?? 0));
|
||||||
|
$wallet_icon = aksrt_icon_markup('zibsvg-money-color-2');
|
||||||
|
$points_icon = aksrt_icon_markup('zibsvg-points-color');
|
||||||
|
$arrow_icon = aksrt_icon_markup('zibsvg-right');
|
||||||
?>
|
?>
|
||||||
<div class="uc-sidebar__pair uc-sidebar__pair--assets">
|
<div class="uc-sidebar__pair uc-sidebar__pair--assets">
|
||||||
<?php if ($show_wallet) : ?>
|
<?php if ($show_wallet) : ?>
|
||||||
<a class="uc-summary-card uc-summary-card--wallet" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
<a class="uc-summary-card uc-summary-card--wallet" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
||||||
<span class="uc-summary-card__head"><?php esc_html_e('账户余额', 'aksrt'); ?><i class="fa fas fa-angle-right" aria-hidden="true"></i></span>
|
<span class="uc-summary-card__head"><?php esc_html_e('账户余额', 'aksrt'); ?><?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span class="uc-summary-card__body">
|
<span class="uc-summary-card__body">
|
||||||
<strong data-aksrt-wallet-value><?php echo esc_html($wallet); ?></strong>
|
<strong data-aksrt-wallet-value><?php echo esc_html($wallet); ?></strong>
|
||||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
<?php echo $wallet_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($show_credits) : ?>
|
<?php if ($show_credits) : ?>
|
||||||
<a class="uc-summary-card uc-summary-card--points" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
<a class="uc-summary-card uc-summary-card--points" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
||||||
<span class="uc-summary-card__head"><?php echo esc_html((string) ($currency_name ?? __('积分', 'aksrt'))); ?><i class="fa fas fa-angle-right" aria-hidden="true"></i></span>
|
<span class="uc-summary-card__head"><?php echo esc_html((string) ($currency_name ?? __('积分', 'aksrt'))); ?><?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span class="uc-summary-card__body">
|
<span class="uc-summary-card__body">
|
||||||
<strong data-aksrt-points-value data-credits-value><?php echo esc_html(aksrt_format_points($points)); ?></strong>
|
<strong data-aksrt-points-value data-credits-value><?php echo esc_html(aksrt_format_points($points)); ?></strong>
|
||||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
<?php echo $points_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -13,20 +13,23 @@ $revenue_url = isset($dashboard_tabs['revenue']) ? aksrt_get_user_center_tab_url
|
|||||||
if ($creation_url === '' && $revenue_url === '') {
|
if ($creation_url === '' && $revenue_url === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$creation_icon = aksrt_icon_markup('zibsvg-merchant-color');
|
||||||
|
$revenue_icon = aksrt_icon_markup('zibsvg-money-color');
|
||||||
|
$arrow_icon = aksrt_icon_markup('zibsvg-right');
|
||||||
?>
|
?>
|
||||||
<div class="uc-sidebar__pair uc-sidebar__pair--centers">
|
<div class="uc-sidebar__pair uc-sidebar__pair--centers">
|
||||||
<?php if ($creation_url !== '') : ?>
|
<?php if ($creation_url !== '') : ?>
|
||||||
<a class="uc-center-card uc-center-card--creation" href="<?php echo esc_url($creation_url); ?>" data-tab="creator">
|
<a class="uc-center-card uc-center-card--creation" href="<?php echo esc_url($creation_url); ?>" data-tab="creator">
|
||||||
<span class="uc-center-card__icon"><i class="fa fas fa-pen" aria-hidden="true"></i></span>
|
<span class="uc-center-card__icon"><?php echo $creation_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span><strong><?php esc_html_e('创作收益', 'aksrt'); ?></strong><small><?php esc_html_e('作者收益明细', 'aksrt'); ?></small></span>
|
<span><strong><?php esc_html_e('创作收益', 'aksrt'); ?></strong><small><?php esc_html_e('作者收益明细', 'aksrt'); ?></small></span>
|
||||||
<i class="fa fas fa-angle-right" aria-hidden="true"></i>
|
<?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($revenue_url !== '') : ?>
|
<?php if ($revenue_url !== '') : ?>
|
||||||
<a class="uc-center-card uc-center-card--revenue" href="<?php echo esc_url($revenue_url); ?>" data-tab="revenue">
|
<a class="uc-center-card uc-center-card--revenue" href="<?php echo esc_url($revenue_url); ?>" data-tab="revenue">
|
||||||
<span class="uc-center-card__icon"><i class="fa fas fa-chart-line" aria-hidden="true"></i></span>
|
<span class="uc-center-card__icon"><?php echo $revenue_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span><strong><?php esc_html_e('推广收益', 'aksrt'); ?></strong><small><?php esc_html_e('推广佣金明细', 'aksrt'); ?></small></span>
|
<span><strong><?php esc_html_e('推广收益', 'aksrt'); ?></strong><small><?php esc_html_e('推广佣金明细', 'aksrt'); ?></small></span>
|
||||||
<i class="fa fas fa-angle-right" aria-hidden="true"></i>
|
<?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* User center sidebar: services and account settings.
|
* User center sidebar: services and account settings.
|
||||||
*
|
*
|
||||||
|
* Icon set mirrors the reference theme user-center sidebar: every entry renders
|
||||||
|
* an inline SVG from the theme sprite through aksrt_icon_markup().
|
||||||
|
*
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -17,22 +20,20 @@ $orders_url = $orders_available ? (string) ($orders_url ?? '') : '';
|
|||||||
$services = array_values(array_filter([
|
$services = array_values(array_filter([
|
||||||
[
|
[
|
||||||
'label' => __('我的等级', 'aksrt'),
|
'label' => __('我的等级', 'aksrt'),
|
||||||
'icon' => 'chart-line',
|
'icon' => 'trend-color',
|
||||||
'tone' => 'level',
|
|
||||||
'url' => $level_url,
|
'url' => $level_url,
|
||||||
|
'tab' => 'level',
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'label' => __('官方认证', 'aksrt'),
|
'label' => __('官方认证', 'aksrt'),
|
||||||
'icon' => 'id-badge',
|
'icon' => 'user-auth',
|
||||||
'tone' => 'auth',
|
|
||||||
'url' => $auth_url,
|
'url' => $auth_url,
|
||||||
'tab' => 'auth',
|
'tab' => 'auth',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('我的订单', 'aksrt'),
|
'label' => __('我的订单', 'aksrt'),
|
||||||
'icon' => 'receipt',
|
'icon' => 'order-color',
|
||||||
'tone' => 'orders',
|
|
||||||
'url' => $orders_url,
|
'url' => $orders_url,
|
||||||
'tab' => 'orders',
|
'tab' => 'orders',
|
||||||
],
|
],
|
||||||
@@ -41,35 +42,38 @@ $services = array_values(array_filter([
|
|||||||
$settings = array_values(array_filter([
|
$settings = array_values(array_filter([
|
||||||
[
|
[
|
||||||
'label' => __('消息通知', 'aksrt'),
|
'label' => __('消息通知', 'aksrt'),
|
||||||
'icon' => 'bell',
|
'icon' => 'msg-color',
|
||||||
'tone' => 'notice',
|
|
||||||
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
||||||
'tab' => 'msg',
|
'tab' => 'msg',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('个人资料', 'aksrt'),
|
'label' => __('个人资料', 'aksrt'),
|
||||||
'icon' => 'user-edit',
|
'icon' => 'user-color',
|
||||||
'tone' => 'profile',
|
|
||||||
'url' => isset($dashboard_tabs['profile']) ? aksrt_get_user_center_tab_url('profile') : '',
|
'url' => isset($dashboard_tabs['profile']) ? aksrt_get_user_center_tab_url('profile') : '',
|
||||||
|
'tab' => 'profile',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('账户安全', 'aksrt'),
|
'label' => __('账户安全', 'aksrt'),
|
||||||
'icon' => 'shield-alt',
|
'icon' => 'security-color',
|
||||||
'tone' => 'security',
|
|
||||||
'url' => isset($dashboard_tabs['settings']) ? aksrt_get_user_center_tab_url('settings') : '',
|
'url' => isset($dashboard_tabs['settings']) ? aksrt_get_user_center_tab_url('settings') : '',
|
||||||
|
'tab' => 'settings',
|
||||||
],
|
],
|
||||||
], static fn(array $item): bool => (string) $item['url'] !== ''));
|
], static fn(array $item): bool => (string) $item['url'] !== ''));
|
||||||
|
|
||||||
$render_grid = static function (array $items): void {
|
$render_grid = static function (array $items): void {
|
||||||
?>
|
?>
|
||||||
<div class="uc-service-grid">
|
<div class="flex ac hh text-center icon-but-box user-icon-but-box">
|
||||||
<?php foreach ($items as $item) : ?>
|
<?php foreach ($items as $item) :
|
||||||
<a class="uc-service-grid__item uc-service-grid__item--<?php echo esc_attr((string) $item['tone']); ?><?php echo !empty($item['tab']) && ($active_tab ?? '') === $item['tab'] ? ' is-active' : ''; ?>" href="<?php echo esc_url((string) $item['url']); ?>"<?php if (!empty($item['tab'])) : ?> data-tab="<?php echo esc_attr((string) $item['tab']); ?>"<?php endif; ?>>
|
$tab = (string) ($item['tab'] ?? '');
|
||||||
<span class="uc-service-grid__icon">
|
$item_class = 'icon-but-' . ($tab !== '' ? $tab : 'item');
|
||||||
<i class="fa fas fa-<?php echo esc_attr((string) $item['icon']); ?>" aria-hidden="true"></i>
|
if (($active_tab ?? '') === $tab) {
|
||||||
<?php if (!empty($item['count'])) : ?><small><?php echo esc_html((string) $item['count']); ?></small><?php endif; ?>
|
$item_class .= ' is-active';
|
||||||
</span>
|
}
|
||||||
<span><?php echo esc_html((string) $item['label']); ?></span>
|
$icon_html = aksrt_icon_markup('zibsvg-' . (string) $item['icon']);
|
||||||
|
?>
|
||||||
|
<a class="<?php echo esc_attr($item_class); ?>" href="<?php echo esc_url((string) $item['url']); ?>"<?php if ($tab !== '') : ?> data-tab="<?php echo esc_attr($tab); ?>"<?php endif; ?>>
|
||||||
|
<div class="em16"><?php echo $icon_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||||
|
<div class="px12 muted-color mt3"><?php echo esc_html((string) $item['label']); ?></div>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if ($status === '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($vip_level > 0) {
|
if ($vip_level > 0) {
|
||||||
$title = (string) ($vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
$title = (string) ($vip_levels[$vip_level]['name_html'] ?? $vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||||
if ($expires <= 0 && ($vip_state['source'] ?? 'none') !== 'order') {
|
if ($expires <= 0 && ($vip_state['source'] ?? 'none') !== 'order') {
|
||||||
$status = __('永久会员', 'aksrt');
|
$status = __('永久会员', 'aksrt');
|
||||||
} else {
|
} else {
|
||||||
@@ -41,7 +41,7 @@ if ($vip_level > 0) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} elseif ($expired_level > 0) {
|
} elseif ($expired_level > 0) {
|
||||||
$title = (string) ($vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
$title = (string) ($vip_levels[$expired_level]['name_html'] ?? $vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||||
$status = __('已过期', 'aksrt');
|
$status = __('已过期', 'aksrt');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,13 +49,14 @@ $sidebar_level = $vip_level > 0 ? $vip_level : $expired_level;
|
|||||||
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
||||||
$sidebar_icon = $sidebar_icon_url !== ''
|
$sidebar_icon = $sidebar_icon_url !== ''
|
||||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $sidebar_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
? '<img' . aksrt_get_lazy_attr('lazy_other', $sidebar_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||||
: '<i class="fa fas fa-certificate" aria-hidden="true"></i>';
|
: aksrt_icon_markup('zibsvg-vip_1');
|
||||||
|
$sidebar_arrow = aksrt_icon_markup('zibsvg-right', 'uc-sidebar-vip__arrow');
|
||||||
?>
|
?>
|
||||||
<a class="uc-sidebar-vip" href="<?php echo esc_url($vip_url); ?>" data-tab="vip">
|
<a class="uc-sidebar-vip" href="<?php echo esc_url($vip_url); ?>" data-tab="vip">
|
||||||
<span class="uc-sidebar-vip__icon"><?php echo $sidebar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
<span class="uc-sidebar-vip__icon"><?php echo $sidebar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||||
<span class="uc-sidebar-vip__content">
|
<span class="uc-sidebar-vip__content">
|
||||||
<strong><?php echo esc_html($title); ?></strong>
|
<strong><?php echo wp_kses_post($title); ?></strong>
|
||||||
<small><?php echo esc_html($status); ?></small>
|
<small><?php echo wp_kses_post($status); ?></small>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa fas fa-angle-right uc-sidebar-vip__arrow" aria-hidden="true"></i>
|
<?php echo $sidebar_arrow; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
</a>
|
</a>
|
||||||
@@ -29,6 +29,8 @@ $vip_state_name = (string) ($vip_state['state'] ?? 'unopened');
|
|||||||
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
||||||
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
||||||
$desc_top = (string) ($vip_settings['desc'] ?? '');
|
$desc_top = (string) ($vip_settings['desc'] ?? '');
|
||||||
|
$renew_desc = (string) ($vip_settings['renew']['desc'] ?? '');
|
||||||
|
$upgrade_desc = (string) ($vip_settings['upgrade']['desc'] ?? '');
|
||||||
|
|
||||||
// Membership summary: title → level → intro → expiry.
|
// Membership summary: title → level → intro → expiry.
|
||||||
$vip_state = 'unopened';
|
$vip_state = 'unopened';
|
||||||
@@ -36,13 +38,15 @@ $vip_state_label = __('未开通', 'aksrt');
|
|||||||
$vip_level_number = 0;
|
$vip_level_number = 0;
|
||||||
$vip_level_name = __('未开通会员', 'aksrt');
|
$vip_level_name = __('未开通会员', 'aksrt');
|
||||||
$vip_description = $desc_top !== '' ? $desc_top : __('开通会员,尊享专属特权', 'aksrt');
|
$vip_description = $desc_top !== '' ? $desc_top : __('开通会员,尊享专属特权', 'aksrt');
|
||||||
$vip_expiry_text = __('到期时间:未开通', 'aksrt');
|
// 未开通时不留到期文案:状态标签已显示「未开通」,参考主题对从未开通
|
||||||
|
// 用户同样不输出任何到期信息(仅生效中/永久/已过期才有)。
|
||||||
|
$vip_expiry_text = '';
|
||||||
$vip_is_permanent = false;
|
$vip_is_permanent = false;
|
||||||
$btn_type = 'pay';
|
$btn_type = 'pay';
|
||||||
|
|
||||||
if ($level > 0) {
|
if ($level > 0) {
|
||||||
$vip_level_number = $level;
|
$vip_level_number = $level;
|
||||||
$vip_level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
$vip_level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||||
$level_desc = (string) ($levels[$level]['desc'] ?? '');
|
$level_desc = (string) ($levels[$level]['desc'] ?? '');
|
||||||
$vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt');
|
$vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt');
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ if ($level > 0) {
|
|||||||
$vip_state_label = __('生效中', 'aksrt');
|
$vip_state_label = __('生效中', 'aksrt');
|
||||||
$vip_expiry_text = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires));
|
$vip_expiry_text = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires));
|
||||||
if ($base_level > 0 && $base_level < $level) {
|
if ($base_level > 0 && $base_level < $level) {
|
||||||
$base_name = (string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level));
|
$base_name = wp_strip_all_tags((string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level)));
|
||||||
$base_expiry_text = $base_expires > 0
|
$base_expiry_text = $base_expires > 0
|
||||||
? wp_date('Y年m月d日', $base_expires)
|
? wp_date('Y年m月d日', $base_expires)
|
||||||
: __('永久', 'aksrt');
|
: __('永久', 'aksrt');
|
||||||
@@ -71,7 +75,7 @@ if ($level > 0) {
|
|||||||
$vip_state = 'expired';
|
$vip_state = 'expired';
|
||||||
$vip_state_label = __('已过期', 'aksrt');
|
$vip_state_label = __('已过期', 'aksrt');
|
||||||
$vip_level_number = $expired_level;
|
$vip_level_number = $expired_level;
|
||||||
$vip_level_name = (string) ($levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
$vip_level_name = (string) ($levels[$expired_level]['name_html'] ?? $levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||||
$expired_desc = (string) ($levels[$expired_level]['desc'] ?? '');
|
$expired_desc = (string) ($levels[$expired_level]['desc'] ?? '');
|
||||||
$vip_description = $expired_desc !== ''
|
$vip_description = $expired_desc !== ''
|
||||||
? $expired_desc
|
? $expired_desc
|
||||||
@@ -91,11 +95,12 @@ $status_html = '<div class="uc-vip-status" role="status">'
|
|||||||
. '<span class="uc-vip-status__state">' . esc_html($vip_state_label) . '</span>'
|
. '<span class="uc-vip-status__state">' . esc_html($vip_state_label) . '</span>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '<div class="uc-vip-status__main uc-vip-status__main--level-' . (int) $vip_level_number . '">'
|
. '<div class="uc-vip-status__main uc-vip-status__main--level-' . (int) $vip_level_number . '">'
|
||||||
. '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>'
|
// 未开通(level 0)不输出图标位:名称前不放通用证书图标。
|
||||||
. '<span class="uc-vip-status__level">' . esc_html($vip_level_name) . '</span>'
|
. ($vip_level_number > 0 ? '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>' : '')
|
||||||
|
. '<span class="uc-vip-status__level">' . wp_kses_post($vip_level_name) . '</span>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '<div class="uc-vip-status__desc">' . esc_html($vip_description) . '</div>'
|
. '<div class="uc-vip-status__desc">' . wp_kses_post($vip_description) . '</div>'
|
||||||
. '<div class="uc-vip-status__expiry"><i class="fa fas fa-clock" aria-hidden="true"></i><span>' . esc_html($vip_expiry_text) . '</span></div>'
|
. ($vip_expiry_text !== '' ? '<div class="uc-vip-status__expiry"><i class="fa fas fa-clock" aria-hidden="true"></i><span>' . esc_html($vip_expiry_text) . '</span></div>' : '')
|
||||||
. '</div>';
|
. '</div>';
|
||||||
|
|
||||||
// Level cards.
|
// Level cards.
|
||||||
@@ -104,15 +109,18 @@ foreach ([1, 2] as $lv) {
|
|||||||
if (empty($levels[$lv]['enabled'])) {
|
if (empty($levels[$lv]['enabled'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
$lv_name = (string) ($levels[$lv]['name_html'] ?? $levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||||
$lv_desc = (string) ($levels[$lv]['desc'] ?? '');
|
$lv_desc = (string) ($levels[$lv]['desc'] ?? '');
|
||||||
// Preserve admin equity HTML as-is (sanitize/wrap only; never re-split into items).
|
// Preserve admin equity HTML as-is (sanitize/wrap only; never re-split into items).
|
||||||
$equity_raw = (string) ($levels[$lv]['equity'] ?? '');
|
$equity_raw = (string) ($levels[$lv]['equity'] ?? '');
|
||||||
$equity = function_exists('aksrt_format_vip_equity_html')
|
$equity = function_exists('aksrt_format_vip_equity_html')
|
||||||
? aksrt_format_vip_equity_html($equity_raw)
|
? aksrt_format_vip_equity_html($equity_raw)
|
||||||
: '';
|
: '';
|
||||||
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
$cta = __('开通', 'aksrt') . wp_kses_post($lv_name);
|
||||||
$vip_attr = $lv;
|
$vip_attr = $lv;
|
||||||
|
// 参考主题 zibpay_get_vip_card:续费/升级档位卡展示各自的一句话简介。
|
||||||
|
$card_action = 'pay';
|
||||||
|
$card_desc = $lv_desc;
|
||||||
$lv_icon_url = esc_url((string) ($levels[$lv]['img_icon'] ?? ''));
|
$lv_icon_url = esc_url((string) ($levels[$lv]['img_icon'] ?? ''));
|
||||||
$lv_icon = $lv_icon_url !== ''
|
$lv_icon = $lv_icon_url !== ''
|
||||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||||
@@ -120,9 +128,11 @@ foreach ([1, 2] as $lv) {
|
|||||||
if (str_starts_with($btn_type, 'renew_')) {
|
if (str_starts_with($btn_type, 'renew_')) {
|
||||||
$renew_lv = (int) substr($btn_type, 6);
|
$renew_lv = (int) substr($btn_type, 6);
|
||||||
if ($lv === $renew_lv) {
|
if ($lv === $renew_lv) {
|
||||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
$cta = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'renew';
|
||||||
} elseif ($lv > $renew_lv) {
|
} elseif ($lv > $renew_lv) {
|
||||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'upgrade';
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -130,25 +140,32 @@ foreach ([1, 2] as $lv) {
|
|||||||
if ($lv <= $level) {
|
if ($lv <= $level) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'upgrade';
|
||||||
$vip_attr = 2;
|
$vip_attr = 2;
|
||||||
} elseif ($level > 0 && $lv < $level) {
|
} elseif ($level > 0 && $lv < $level) {
|
||||||
continue;
|
continue;
|
||||||
} elseif ($level > 0 && $lv === $level) {
|
} elseif ($level > 0 && $lv === $level) {
|
||||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
$cta = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||||
|
$card_action = 'renew';
|
||||||
|
}
|
||||||
|
if ($card_action === 'renew' && $renew_desc !== '') {
|
||||||
|
$card_desc = $renew_desc;
|
||||||
|
} elseif ($card_action === 'upgrade' && $upgrade_desc !== '') {
|
||||||
|
$card_desc = $upgrade_desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cards .= '<div class="aksrt-panel text-center pay-vip-info flex1 level-' . $lv . ' uc-vip-level-card">';
|
$cards .= '<div class="aksrt-panel text-center pay-vip-info flex1 level-' . $lv . ' uc-vip-level-card">';
|
||||||
$cards .= '<div class="relative uc-vip-level-card__inner">';
|
$cards .= '<div class="relative uc-vip-level-card__inner">';
|
||||||
$cards .= '<div class="v-iconbox mb10">';
|
$cards .= '<div class="v-iconbox mb10">';
|
||||||
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . esc_html($lv_name) . '</div>';
|
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . wp_kses_post($lv_name) . '</div>';
|
||||||
if ($lv_desc !== '') {
|
if ($card_desc !== '') {
|
||||||
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($lv_desc) . '</div>';
|
$cards .= '<div class="v-desc em09 muted-2-color">' . wp_kses_post($card_desc) . '</div>';
|
||||||
}
|
}
|
||||||
$cards .= '</div>';
|
$cards .= '</div>';
|
||||||
$cards .= $equity;
|
$cards .= $equity;
|
||||||
$cards .= '<a class="padding-lg but jb-vip' . $lv . ' radius pay-vip uc-vip-cta" href="javascript:;" vip-level="' . (int) $vip_attr . '">'
|
$cards .= '<a class="padding-lg but jb-vip' . $lv . ' radius pay-vip uc-vip-cta" href="javascript:;" vip-level="' . (int) $vip_attr . '">'
|
||||||
. '<span>' . esc_html($cta) . '</span>'
|
. '<span>' . $cta . '</span>'
|
||||||
. ' <i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></a>';
|
. ' <i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></a>';
|
||||||
$cards .= '</div></div>';
|
$cards .= '</div></div>';
|
||||||
}
|
}
|
||||||
@@ -169,10 +186,4 @@ foreach ([1, 2] as $lv) {
|
|||||||
// 下方的全宽横条;仅非会员 + 积分兑换开启 + 有可兑换套餐时输出)。
|
// 下方的全宽横条;仅非会员 + 积分兑换开启 + 有可兑换套餐时输出)。
|
||||||
echo aksrt_get_vip_points_exchange_entry_html($user_id, $level); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
|
echo aksrt_get_vip_points_exchange_entry_html($user_id, $level); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if (!empty($vip_settings['more'])) : ?>
|
|
||||||
<div class="muted-2-color em09 muted-box padding-10 mini-radius mt15">
|
|
||||||
<?php echo $vip_settings['more']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -375,6 +375,8 @@ function aksrt_captcha_scenes(): array {
|
|||||||
'signin' => __('每日签到', 'aksrt'),
|
'signin' => __('每日签到', 'aksrt'),
|
||||||
'front_submit' => __('前台投稿', 'aksrt'),
|
'front_submit' => __('前台投稿', 'aksrt'),
|
||||||
'tip_user' => __('打赏用户', 'aksrt'),
|
'tip_user' => __('打赏用户', 'aksrt'),
|
||||||
|
'after_sale_apply' => __('申请售后', 'aksrt'),
|
||||||
|
'after_sale_cancel' => __('撤销售后', 'aksrt'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-105
@@ -19,30 +19,12 @@ class AIController extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function register_routes(): void {
|
public function register_routes(): void {
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/status', [
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => [$this, 'get_status'],
|
|
||||||
'permission_callback' => [$this, 'check_logged_in'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/settings', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'update_settings'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'seo_generate'],
|
'callback' => [$this, 'seo_generate'],
|
||||||
'permission_callback' => [$this, 'check_logged_in'],
|
'permission_callback' => [$this, 'check_logged_in'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'seo_suggest'],
|
|
||||||
'permission_callback' => [$this, 'check_write_assist'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'summary_suggest'],
|
'callback' => [$this, 'summary_suggest'],
|
||||||
@@ -76,66 +58,6 @@ class AIController extends ControllerBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_status(): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
|
||||||
return $this->success(['data' => [
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => 'none',
|
|
||||||
'configured' => false,
|
|
||||||
'available' => false,
|
|
||||||
'backend' => 'none',
|
|
||||||
]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(['data' => \AKSRT\Ai\AI::get_instance()->get_status()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update_settings(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
|
||||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$allowed = [
|
|
||||||
'enabled' => 'bool',
|
|
||||||
'provider' => 'string',
|
|
||||||
'api_key' => 'string',
|
|
||||||
'api_base' => 'url',
|
|
||||||
'model' => 'string',
|
|
||||||
'max_tokens' => 'int',
|
|
||||||
'rate_limit' => 'int',
|
|
||||||
];
|
|
||||||
|
|
||||||
$config = [];
|
|
||||||
foreach ($allowed as $key => $type) {
|
|
||||||
if (!isset($params[$key])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch ($type) {
|
|
||||||
case 'bool':
|
|
||||||
$config[$key] = !empty($params[$key]);
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$config[$key] = (int) $params[$key];
|
|
||||||
break;
|
|
||||||
case 'url':
|
|
||||||
$config[$key] = esc_url_raw((string) $params[$key]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$config[$key] = sanitize_text_field((string) $params[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$success = \AKSRT\Ai\AI::get_instance()->update_config($config);
|
|
||||||
if ($success) {
|
|
||||||
return $this->success([
|
|
||||||
'message' => __('AI 设置已保存', 'aksrt'),
|
|
||||||
'data' => \AKSRT\Ai\AI::get_instance()->get_status(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $this->error(__('保存失败', 'aksrt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
||||||
*/
|
*/
|
||||||
@@ -170,32 +92,6 @@ class AIController extends ControllerBase {
|
|||||||
return $this->success(['data' => $result['data']]);
|
return $this->success(['data' => $result['data']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seo_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AISEO')) {
|
|
||||||
return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
|
||||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
|
||||||
$post_id = absint($params['post_id'] ?? 0);
|
|
||||||
|
|
||||||
if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
|
|
||||||
return $this->error(__('权限不足', 'aksrt'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($title === '') {
|
|
||||||
return $this->error(__('标题不能为空', 'aksrt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = \AKSRT\Ai\AISEO::get_instance()->suggest($title, $content);
|
|
||||||
if (!empty($result['success'])) {
|
|
||||||
return $this->success(['data' => $result['data']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||||
@@ -224,7 +120,7 @@ class AIController extends ControllerBase {
|
|||||||
$ai = \AKSRT\Ai\AI::get_instance();
|
$ai = \AKSRT\Ai\AI::get_instance();
|
||||||
if (!$ai->is_available()) {
|
if (!$ai->is_available()) {
|
||||||
return $this->error(
|
return $this->error(
|
||||||
__('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'),
|
__('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||||
503
|
503
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package AKSRT
|
||||||
|
*
|
||||||
|
* 后台列表页分页条 —— 1:1 复刻「卡密管理」页的 zibpay_admin_pagenavi 结构。
|
||||||
|
*
|
||||||
|
* 支付中心各页面(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 / 提现审核 / 卡密管理)
|
||||||
|
* 原先各写一份几乎相同的翻页代码。这里统一到一处,标记沿用卡密管理那一套:
|
||||||
|
* `« ‹ 第 X 页,共 Y 页 › »` 四枚按钮 + 「每页 X 共Y」计数,没有数字页码链接。
|
||||||
|
*
|
||||||
|
* 与参考实现的两处差异,均为缺陷修复:
|
||||||
|
* 1) 基准 URL 显式以「page 参数 + 当前筛选参数」拼装。历史写法里 add_query_arg('paged', N)
|
||||||
|
* 只有两个参数时会落到 $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态
|
||||||
|
* 漂移,列条件也容易丢。
|
||||||
|
* 2) 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导。历史写法在此处会留下
|
||||||
|
* 「第 2 页 / 共 1 页」这类越界状态,四个分支一个都不命中,上一页/下一页同时停在
|
||||||
|
* '#':按钮既不禁用也点不动。
|
||||||
|
*
|
||||||
|
* 单页也渲染分页条(只要总数 > 0):与参考实现一致,运营能直接看到「每页 20 共 5」
|
||||||
|
* 这样的口径,四枚按钮全 disabled 也明确了「没有更多页」。
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin\Native;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class AdminListPagination {
|
||||||
|
|
||||||
|
/** 每页条数默认值。所有列表页一致,且不提供「每页显示」控件。 */
|
||||||
|
public const DEFAULT_PER_PAGE = 20;
|
||||||
|
|
||||||
|
/** @var bool 内联样式只输出一次 */
|
||||||
|
private static $assets_printed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染分页条。
|
||||||
|
*
|
||||||
|
* 真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成,这里只把页码
|
||||||
|
* 夹到有效范围,保证显示状态自洽。
|
||||||
|
*
|
||||||
|
* @param string $page_slug 当前 admin.php 的 page 参数。
|
||||||
|
* @param int $total_count 当前筛选条件下的记录总数。
|
||||||
|
* @param int $current_page 当前页码(未经夹取亦可)。
|
||||||
|
* @param int $per_page 每页条数。
|
||||||
|
* @param array<string, string|int> $query_args 当前筛选参数,不含 paged。
|
||||||
|
*/
|
||||||
|
public static function render(
|
||||||
|
string $page_slug,
|
||||||
|
int $total_count,
|
||||||
|
int $current_page,
|
||||||
|
int $per_page,
|
||||||
|
array $query_args = []
|
||||||
|
): void {
|
||||||
|
if ($total_count <= 0 || $per_page <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::print_assets();
|
||||||
|
|
||||||
|
$total_pages = max(1, (int) ceil($total_count / $per_page));
|
||||||
|
$current_page = min(max(1, $current_page), $total_pages);
|
||||||
|
|
||||||
|
// 显式基准 URL:不再让 add_query_arg() 只有两个参数时退回 $_SERVER['REQUEST_URI']。
|
||||||
|
$base = add_query_arg(array_merge(['page' => $page_slug], $query_args), admin_url('admin.php'));
|
||||||
|
$page_url = static function (int $page) use ($base): string {
|
||||||
|
return add_query_arg('paged', $page, $base);
|
||||||
|
};
|
||||||
|
|
||||||
|
$on_first = $current_page <= 1;
|
||||||
|
$on_last = $current_page >= $total_pages;
|
||||||
|
|
||||||
|
$first_page_url = $on_first ? '#' : $page_url(1);
|
||||||
|
$prev_page_url = $on_first ? '#' : $page_url($current_page - 1);
|
||||||
|
$next_page_url = $on_last ? '#' : $page_url($current_page + 1);
|
||||||
|
$last_page_url = $on_last ? '#' : $page_url($total_pages);
|
||||||
|
?>
|
||||||
|
<div class="tablenav bottom">
|
||||||
|
<div class="tablenav-pages">
|
||||||
|
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $per_page), esc_html((string) $total_count)); ?></span>
|
||||||
|
<span class="pagination-links">
|
||||||
|
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
|
||||||
|
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
|
||||||
|
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
|
||||||
|
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
|
||||||
|
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行内样式。
|
||||||
|
*
|
||||||
|
* 这些原生后台页面不加载主题 CSS(admin-main.css 只在 edit.php 文章列表入队),
|
||||||
|
* 所以分页条依赖的规则随标记一起内联输出。
|
||||||
|
*/
|
||||||
|
private static function print_assets(): void {
|
||||||
|
if (self::$assets_printed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$assets_printed = true;
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
||||||
|
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
||||||
|
.tablenav-pages a.disabled { pointer-events: none; }
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,9 +21,6 @@ class PayChargeCards {
|
|||||||
public const NONCE = 'aksrt_card_pass_page';
|
public const NONCE = 'aksrt_card_pass_page';
|
||||||
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
|
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
|
||||||
|
|
||||||
/** 每页条数默认值;列表页可由 ice_perpage 覆盖(无界面控件,仅供 URL / 程序化调用)。 */
|
|
||||||
public const DEFAULT_PER_PAGE = 20;
|
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
/** @var string 当前页面 URL */
|
/** @var string 当前页面 URL */
|
||||||
@@ -119,7 +116,8 @@ class PayChargeCards {
|
|||||||
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time';
|
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time';
|
||||||
$order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC';
|
$order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC';
|
||||||
$paged = max(1, (int) ($_GET['paged'] ?? 1));
|
$paged = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? self::DEFAULT_PER_PAGE)));
|
// 每页条数默认值取自共享助手;ice_perpage 仅作为 URL 参数被尊重,无界面控件。
|
||||||
|
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? AdminListPagination::DEFAULT_PER_PAGE)));
|
||||||
|
|
||||||
// 参考主题:status=0 表示未使用;本主题规范状态为 active。
|
// 参考主题:status=0 表示未使用;本主题规范状态为 active。
|
||||||
if ($status === '0' || $status === 'active') {
|
if ($status === '0' || $status === 'active') {
|
||||||
@@ -297,7 +295,7 @@ class PayChargeCards {
|
|||||||
echo '</form>';
|
echo '</form>';
|
||||||
echo '<div class="clear"></div>';
|
echo '<div class="clear"></div>';
|
||||||
|
|
||||||
$this->render_pagenavi($count_all, $per_page, $query_args);
|
AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
@@ -1059,67 +1057,18 @@ class PayChargeCards {
|
|||||||
if ($order === 'ASC') {
|
if ($order === 'ASC') {
|
||||||
$args['desc'] = 'ASC';
|
$args['desc'] = 'ASC';
|
||||||
}
|
}
|
||||||
if ($per_page !== self::DEFAULT_PER_PAGE) {
|
if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) {
|
||||||
$args['ice_perpage'] = (string) $per_page;
|
$args['ice_perpage'] = (string) $per_page;
|
||||||
}
|
}
|
||||||
return $args;
|
return $args;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。
|
|
||||||
*
|
|
||||||
* 与参考实现的两处差异,均为缺陷修复:
|
|
||||||
* 1) 页码先夹到 [1, total_pages]。历史写法在此处会出现「第 2 页 / 共 1 页」这类越界状态,
|
|
||||||
* 四个分支一个都不命中,上一页/下一页同时停在 '#',按钮既不禁用也点不动。
|
|
||||||
* 2) 链接基于显式基准 URL(page_url + 当前筛选条件),不再依赖 REQUEST_URI。
|
|
||||||
*
|
|
||||||
* @param array<string,string> $query_args 当前筛选参数(不含 paged)。
|
|
||||||
*/
|
|
||||||
private function render_pagenavi(int $total_count, int $number_per_page, array $query_args = []): void {
|
|
||||||
if ($total_count <= 0 || $number_per_page <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$total_pages = max(1, (int) ceil($total_count / $number_per_page));
|
|
||||||
$current_page = min(max(1, (int) ($_GET['paged'] ?? 1)), $total_pages);
|
|
||||||
|
|
||||||
$base = $query_args ? add_query_arg($query_args, $this->page_url) : $this->page_url;
|
|
||||||
$page_url = static function (int $page) use ($base): string {
|
|
||||||
return add_query_arg('paged', $page, $base);
|
|
||||||
};
|
|
||||||
|
|
||||||
$on_first = $current_page <= 1;
|
|
||||||
$on_last = $current_page >= $total_pages;
|
|
||||||
|
|
||||||
$first_page_url = $on_first ? '#' : $page_url(1);
|
|
||||||
$prev_page_url = $on_first ? '#' : $page_url($current_page - 1);
|
|
||||||
$next_page_url = $on_last ? '#' : $page_url($current_page + 1);
|
|
||||||
$last_page_url = $on_last ? '#' : $page_url($total_pages);
|
|
||||||
?>
|
|
||||||
<div class="tablenav bottom">
|
|
||||||
<div class="tablenav-pages">
|
|
||||||
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $number_per_page), esc_html((string) $total_count)); ?></span>
|
|
||||||
<span class="pagination-links">
|
|
||||||
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
|
|
||||||
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
|
|
||||||
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
|
|
||||||
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
|
|
||||||
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
|
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
|
||||||
private function print_assets(): void {
|
private function print_assets(): void {
|
||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||||||
.order-header .subsubsub { margin: 6px 0; }
|
.order-header .subsubsub { margin: 6px 0; }
|
||||||
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
|
||||||
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
|
||||||
.tablenav-pages a.disabled { pointer-events: none; }
|
|
||||||
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
||||||
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
||||||
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class PayCoupons {
|
|||||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
$type_labels = [
|
$type_labels = [
|
||||||
'fixed' => __('固定金额', 'aksrt'),
|
'fixed' => __('固定金额', 'aksrt'),
|
||||||
'percent' => __('折扣百分比', 'aksrt'),
|
'percent' => __('折扣百分比', 'aksrt'),
|
||||||
@@ -199,7 +199,21 @@ class PayCoupons {
|
|||||||
'offset' => ($page_num - 1) * $per_page,
|
'offset' => ($page_num - 1) * $per_page,
|
||||||
]);
|
]);
|
||||||
$items = $result['items'] ?? [];
|
$items = $result['items'] ?? [];
|
||||||
$pages = max(1, (int) ($result['total_pages'] ?? 1));
|
$total = (int) ($result['total'] ?? 0);
|
||||||
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$result = $svc->get_list([
|
||||||
|
'status' => $status,
|
||||||
|
'search' => $search,
|
||||||
|
'limit' => $per_page,
|
||||||
|
'offset' => ($page_num - 1) * $per_page,
|
||||||
|
]);
|
||||||
|
$items = $result['items'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||||
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||||
@@ -240,8 +254,16 @@ class PayCoupons {
|
|||||||
}
|
}
|
||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'status' => $status !== '' ? $status : null,
|
||||||
|
's' => $search !== '' ? $search : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->render_pagination($pages, $page_num);
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_create_coupon(): void {
|
public function handle_create_coupon(): void {
|
||||||
@@ -457,20 +479,4 @@ class PayCoupons {
|
|||||||
private function page_url(array $args = []): string {
|
private function page_url(array $args = []): string {
|
||||||
return add_query_arg(array_merge(['page' => self::PAGE_SLUG], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => self::PAGE_SLUG], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package AKSRT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace AKSRT\Admin\Native;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
class PayDownloadLogs {
|
|
||||||
|
|
||||||
public const CAPABILITY = 'manage_options';
|
|
||||||
public const PAGE_SLUG = 'aksrt_download_logs';
|
|
||||||
public const NONCE = 'aksrt_download_logs_bulk';
|
|
||||||
|
|
||||||
/** 单次批量删除的令牌上限,与列表每页条数量级一致,防止超大 IN 查询。 */
|
|
||||||
private const BULK_DELETE_LIMIT = 100;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// 页面动作(批量删除)在 render_page() 渲染前就地处理,无需额外钩子。
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render_page(): void {
|
|
||||||
$this->ensure_cap();
|
|
||||||
|
|
||||||
if ($this->is_bulk_delete_request()) {
|
|
||||||
$this->handle_bulk_delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '<div class="wrap"><h1>' . esc_html__('下载记录', 'aksrt') . '</h1>';
|
|
||||||
|
|
||||||
if (isset($_GET['dl_deleted'])) {
|
|
||||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html(sprintf(
|
|
||||||
/* translators: %d: deleted record count */
|
|
||||||
__('已删除 %d 条下载记录。', 'aksrt'),
|
|
||||||
max(0, (int) $_GET['dl_deleted'])
|
|
||||||
)) . '</p></div>';
|
|
||||||
} elseif (isset($_GET['dl_empty'])) {
|
|
||||||
echo '<div class="notice notice-error"><p>' . esc_html__('未选择需要删除的记录。', 'aksrt') . '</p></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
|
||||||
echo '<div class="notice notice-error"><p>' . esc_html__('数据库服务不可用。', 'aksrt') . '</p></div></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
|
||||||
if ($table === '') {
|
|
||||||
echo '<div class="notice notice-error"><p>' . esc_html__('下载令牌表未初始化。', 'aksrt') . '</p></div></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$post_id = isset($_GET['post_id']) ? max(0, (int) $_GET['post_id']) : 0;
|
|
||||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
|
|
||||||
$only_used = !isset($_GET['used']) || (string) $_GET['used'] !== '0';
|
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
|
||||||
$per_page = 30;
|
|
||||||
|
|
||||||
$result = $this->query_logs($table, [
|
|
||||||
'post_id' => $post_id,
|
|
||||||
'user_id' => $user_id,
|
|
||||||
'only_used' => $only_used,
|
|
||||||
'limit' => $per_page,
|
|
||||||
'offset' => ($page_num - 1) * $per_page,
|
|
||||||
]);
|
|
||||||
$items = $result['items'];
|
|
||||||
$total = $result['total'];
|
|
||||||
$pages = max(1, (int) ceil($total / $per_page));
|
|
||||||
|
|
||||||
echo '<p class="description">' . esc_html__('基于下载令牌表(aksrt_dl_token)。下载已改为直连模式,不再签发新令牌,以下为历史记录。', 'aksrt') . '</p>';
|
|
||||||
|
|
||||||
echo '<form method="get" style="margin:12px 0;">';
|
|
||||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
|
||||||
echo '<label>' . esc_html__('文章 ID', 'aksrt') . ' <input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" class="small-text" /></label> ';
|
|
||||||
echo '<label>' . esc_html__('用户 ID', 'aksrt') . ' <input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" class="small-text" /></label> ';
|
|
||||||
echo '<label><input type="checkbox" name="used" value="1" ' . checked($only_used, true, false) . ' /> ' . esc_html__('仅已使用', 'aksrt') . '</label> ';
|
|
||||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
|
||||||
echo ' <a class="button" href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG)) . '">' . esc_html__('重置', 'aksrt') . '</a>';
|
|
||||||
echo '</form>';
|
|
||||||
|
|
||||||
// 批量操作(多选 / 单选)包裹列表;删除成功后重定向回当前筛选视图。
|
|
||||||
echo '<form method="post" action="' . esc_url($this->current_url()) . '">';
|
|
||||||
wp_nonce_field(self::NONCE);
|
|
||||||
echo '<div class="tablenav top"><div class="alignleft actions bulkactions">';
|
|
||||||
echo '<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label>';
|
|
||||||
echo '<select name="bulk_action" id="bulk-action-selector-top">';
|
|
||||||
echo '<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>';
|
|
||||||
echo '<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>';
|
|
||||||
echo '</select>';
|
|
||||||
echo '<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">';
|
|
||||||
echo '</div><br class="clear"></div>';
|
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped">';
|
|
||||||
echo '<thead><tr>';
|
|
||||||
echo '<td id="cb" class="manage-column column-cb check-column" style="width:2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox" onclick="document.querySelectorAll(\'tbody [name="dl_token[]"]\').forEach(c=>c.checked=this.checked)"></td>';
|
|
||||||
echo '<th>' . esc_html__('序号', 'aksrt') . '</th>';
|
|
||||||
echo '<th>' . esc_html__('文章', 'aksrt') . '</th>';
|
|
||||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
|
||||||
echo '<th>' . esc_html__('积分消耗', 'aksrt') . '</th>';
|
|
||||||
echo '<th>' . esc_html__('IP', 'aksrt') . '</th>';
|
|
||||||
echo '<th>' . esc_html__('时间', 'aksrt') . '</th>';
|
|
||||||
echo '</tr></thead><tbody>';
|
|
||||||
|
|
||||||
if (!$items) {
|
|
||||||
echo '<tr><td colspan="7">' . esc_html__('暂无下载记录。', 'aksrt') . '</td></tr>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$seq_offset = ($page_num - 1) * $per_page;
|
|
||||||
foreach ($items as $i => $row) {
|
|
||||||
$uid = (int) ($row['user_id'] ?? 0);
|
|
||||||
$pid = (int) ($row['post_id'] ?? 0);
|
|
||||||
$token = (string) ($row['token'] ?? '');
|
|
||||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
|
||||||
$title = $pid > 0 ? get_the_title($pid) : '';
|
|
||||||
if ($title === '') {
|
|
||||||
$title = $pid > 0 ? sprintf(__('(已删除 #%d)', 'aksrt'), $pid) : '—';
|
|
||||||
}
|
|
||||||
$edit = $pid > 0 ? get_edit_post_link($pid, 'raw') : '';
|
|
||||||
|
|
||||||
echo '<tr>';
|
|
||||||
echo '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . esc_attr($token) . '">' . esc_html__('选择', 'aksrt') . '</label><input id="cb-select-' . esc_attr($token) . '" type="checkbox" name="dl_token[]" value="' . esc_attr($token) . '"></th>';
|
|
||||||
echo '<td>' . esc_html((string) ($seq_offset + $i + 1)) . '</td>';
|
|
||||||
echo '<td>';
|
|
||||||
if ($edit) {
|
|
||||||
echo '<a href="' . esc_url($edit) . '">' . esc_html($title) . '</a>';
|
|
||||||
} else {
|
|
||||||
echo esc_html($title);
|
|
||||||
}
|
|
||||||
if ($pid > 0) {
|
|
||||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&post_id=' . $pid)) . '">' . esc_html__('仅此文章', 'aksrt') . '</a>';
|
|
||||||
}
|
|
||||||
echo '</td>';
|
|
||||||
echo '<td>';
|
|
||||||
if ($user) {
|
|
||||||
echo esc_html($user->display_name . ' (#' . $uid . ')');
|
|
||||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&user_id=' . $uid)) . '">' . esc_html__('仅此用户', 'aksrt') . '</a>';
|
|
||||||
} else {
|
|
||||||
echo esc_html((string) $uid);
|
|
||||||
}
|
|
||||||
echo '</td>';
|
|
||||||
echo '<td>' . esc_html((string) (int) ($row['cost'] ?? 0)) . '</td>';
|
|
||||||
echo '<td><code>' . esc_html((string) ($row['ip_bind'] ?? '')) . '</code></td>';
|
|
||||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
|
||||||
echo '</tr>';
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
echo '</form>';
|
|
||||||
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
echo '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前页面 URL(含筛选参数),用于批量表单回帖与删除后重定向。
|
|
||||||
*/
|
|
||||||
private function current_url(): string {
|
|
||||||
$args = ['page' => self::PAGE_SLUG];
|
|
||||||
foreach (['post_id', 'user_id', 'used', 'paged'] as $key) {
|
|
||||||
if (isset($_GET[$key])) {
|
|
||||||
$args[$key] = wp_unslash($_GET[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return add_query_arg($args, admin_url('admin.php'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function is_bulk_delete_request(): bool {
|
|
||||||
return (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST')
|
|
||||||
&& isset($_POST['bulk_action'])
|
|
||||||
&& ((string) wp_unslash($_POST['bulk_action']) === 'delete');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除选中的下载令牌记录;完成后重定向回当前筛选视图,避免刷新重复提交。
|
|
||||||
*/
|
|
||||||
private function handle_bulk_delete(): void {
|
|
||||||
check_admin_referer(self::NONCE);
|
|
||||||
|
|
||||||
$raw = isset($_POST['dl_token']) ? (array) wp_unslash($_POST['dl_token']) : [];
|
|
||||||
$tokens = array_values(array_unique(array_filter($raw, static function ($token): bool {
|
|
||||||
return is_string($token) && preg_match('/^[A-Za-z0-9]{1,64}$/', $token) === 1;
|
|
||||||
})));
|
|
||||||
$tokens = array_slice($tokens, 0, self::BULK_DELETE_LIMIT);
|
|
||||||
|
|
||||||
if (!$tokens) {
|
|
||||||
wp_safe_redirect(add_query_arg('dl_empty', '1', $this->current_url()));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$deleted = $this->delete_tokens($tokens);
|
|
||||||
wp_safe_redirect(add_query_arg('dl_deleted', $deleted, $this->current_url()));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<int,string> $tokens
|
|
||||||
*/
|
|
||||||
private function delete_tokens(array $tokens): int {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
|
||||||
if ($table === '') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$placeholders = implode(',', array_fill(0, count($tokens), '%s'));
|
|
||||||
$sql = "DELETE FROM {$table} WHERE token IN ({$placeholders})";
|
|
||||||
|
|
||||||
return (int) $wpdb->query($wpdb->prepare($sql, $tokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array{post_id?:int,user_id?:int,only_used?:bool,limit?:int,offset?:int} $args
|
|
||||||
* @return array{items: array<int,array<string,mixed>>, total:int}
|
|
||||||
*/
|
|
||||||
private function query_logs(string $table, array $args): array {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$where = ['1=1'];
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
$post_id = (int) ($args['post_id'] ?? 0);
|
|
||||||
if ($post_id > 0) {
|
|
||||||
$where[] = 'post_id = %d';
|
|
||||||
$params[] = $post_id;
|
|
||||||
}
|
|
||||||
$user_id = (int) ($args['user_id'] ?? 0);
|
|
||||||
if ($user_id > 0) {
|
|
||||||
$where[] = 'user_id = %d';
|
|
||||||
$params[] = $user_id;
|
|
||||||
}
|
|
||||||
if (!empty($args['only_used'])) {
|
|
||||||
$where[] = 'used_count > 0';
|
|
||||||
}
|
|
||||||
|
|
||||||
$where_sql = implode(' AND ', $where);
|
|
||||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 30)));
|
|
||||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
|
||||||
|
|
||||||
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
|
||||||
$total = $params
|
|
||||||
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
|
|
||||||
: (int) $wpdb->get_var($count_sql);
|
|
||||||
|
|
||||||
$sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
|
||||||
$list_params = $params;
|
|
||||||
$list_params[] = $limit;
|
|
||||||
$list_params[] = $offset;
|
|
||||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $list_params), ARRAY_A);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'items' => is_array($rows) ? $rows : [],
|
|
||||||
'total' => $total,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensure_cap(): void {
|
|
||||||
if (!current_user_can(self::CAPABILITY)) {
|
|
||||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,7 +51,7 @@ class PayIncome {
|
|||||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
|
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
|
||||||
? \AKSRT\Payment\Order::get_valid_statuses()
|
? \AKSRT\Payment\Order::get_valid_statuses()
|
||||||
: [];
|
: [];
|
||||||
@@ -99,7 +99,7 @@ class PayIncome {
|
|||||||
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
|
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
|
||||||
echo '<input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" placeholder="' . esc_attr__('文章 ID', 'aksrt') . '" class="small-text" /> ';
|
echo '<input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" placeholder="' . esc_attr__('文章 ID', 'aksrt') . '" class="small-text" /> ';
|
||||||
echo '<select name="currency"><option value="">' . esc_html__('全部货币', 'aksrt') . '</option><option value="CNY"' . selected($currency, 'CNY', false) . '>CNY</option><option value="points"' . selected($currency, 'points', false) . '>' . esc_html__('积分', 'aksrt') . '</option></select> ';
|
echo '<select name="currency"><option value="">' . esc_html__('全部货币', 'aksrt') . '</option><option value="CNY"' . selected($currency, 'CNY', false) . '>CNY</option><option value="points"' . selected($currency, 'points', false) . '>' . esc_html__('积分', 'aksrt') . '</option></select> ';
|
||||||
echo '<input type="search" name="order_number" value="' . esc_attr($order_number) . '" placeholder="' . esc_attr__('流水号', 'aksrt') . '" /> ';
|
echo '<input type="search" name="order_number" value="' . esc_attr($order_number) . '" placeholder="' . esc_attr__('订单号', 'aksrt') . '" /> ';
|
||||||
echo '<input type="date" name="date_from" value="' . esc_attr($date_from) . '" /> — <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /> ';
|
echo '<input type="date" name="date_from" value="' . esc_attr($date_from) . '" /> — <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /> ';
|
||||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||||
if ($user_id > 0) {
|
if ($user_id > 0) {
|
||||||
@@ -124,10 +124,34 @@ class PayIncome {
|
|||||||
$result = $svc->get_list($list_args);
|
$result = $svc->get_list($list_args);
|
||||||
$items = $result['items'] ?? [];
|
$items = $result['items'] ?? [];
|
||||||
$total = (int) ($result['total'] ?? 0);
|
$total = (int) ($result['total'] ?? 0);
|
||||||
$pages = max(1, (int) ($result['max_pages'] ?? 1));
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$list_args['offset'] = ($page_num - 1) * $per_page;
|
||||||
|
$result = $svc->get_list($list_args);
|
||||||
|
$items = $result['items'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged)。必须在下面的行循环之前取好:
|
||||||
|
// 循环里 $user_id 会被每行的 author_id 覆盖。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'status' => $status !== '' ? $status : null,
|
||||||
|
'order_status' => $order_status !== '' ? $order_status : null,
|
||||||
|
'user_id' => $user_id > 0 ? $user_id : null,
|
||||||
|
'post_id' => $post_id > 0 ? $post_id : null,
|
||||||
|
'currency' => $currency !== '' ? $currency : null,
|
||||||
|
'order_number' => $order_number !== '' ? $order_number : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||||
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('流水号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||||
echo '</tr></thead><tbody>';
|
echo '</tr></thead><tbody>';
|
||||||
if (!$items) {
|
if (!$items) {
|
||||||
echo '<tr><td colspan="8">' . esc_html__('暂无分成记录', 'aksrt') . '</td></tr>';
|
echo '<tr><td colspan="8">' . esc_html__('暂无分成记录', 'aksrt') . '</td></tr>';
|
||||||
@@ -164,7 +188,7 @@ class PayIncome {
|
|||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->render_pagination($pages, $page_num);
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,19 +280,4 @@ class PayIncome {
|
|||||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,17 @@ class PayMenus {
|
|||||||
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'after_sales' => [
|
||||||
|
'slug' => 'aksrt_after_sales',
|
||||||
|
'title' => __('售后服务', 'aksrt'),
|
||||||
|
'callback' => static function (): void {
|
||||||
|
if (class_exists('AKSRT\Admin\Native\PayOrders')) {
|
||||||
|
PayOrders::get_instance()->render_after_sales_admin_page();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
||||||
|
},
|
||||||
|
],
|
||||||
'products' => [
|
'products' => [
|
||||||
'slug' => 'aksrt_pay_products',
|
'slug' => 'aksrt_pay_products',
|
||||||
'title' => __('付费文章', 'aksrt'),
|
'title' => __('付费文章', 'aksrt'),
|
||||||
@@ -63,17 +74,6 @@ class PayMenus {
|
|||||||
wp_die(esc_html__('付费文章模块未初始化。', 'aksrt'));
|
wp_die(esc_html__('付费文章模块未初始化。', 'aksrt'));
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'download_logs' => [
|
|
||||||
'slug' => 'aksrt_download_logs',
|
|
||||||
'title' => __('下载记录', 'aksrt'),
|
|
||||||
'callback' => static function (): void {
|
|
||||||
if (class_exists('AKSRT\\Admin\\Native\\PayDownloadLogs')) {
|
|
||||||
PayDownloadLogs::get_instance()->render_page();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
wp_die(esc_html__('下载记录模块未初始化。', 'aksrt'));
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'coupons' => [
|
'coupons' => [
|
||||||
'slug' => 'aksrt_coupons',
|
'slug' => 'aksrt_coupons',
|
||||||
'title' => __('优惠码', 'aksrt'),
|
'title' => __('优惠码', 'aksrt'),
|
||||||
|
|||||||
+423
-130
@@ -11,6 +11,7 @@ class PayOrders {
|
|||||||
|
|
||||||
public const CAPABILITY = 'manage_options';
|
public const CAPABILITY = 'manage_options';
|
||||||
public const PAGE_SLUG = 'aksrt_orders';
|
public const PAGE_SLUG = 'aksrt_orders';
|
||||||
|
public const PAGE_AFTER_SALES_SLUG = 'aksrt_after_sales';
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ class PayOrders {
|
|||||||
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
|
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
|
||||||
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
|
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
|
||||||
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
|
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
|
||||||
add_action('admin_post_aksrt_after_sale_review', [$this, 'handle_after_sale_review']);
|
add_action('admin_post_aksrt_pay_refund_order', [$this, 'handle_refund_order']);
|
||||||
|
add_action('admin_post_aksrt_after_sale_update', [$this, 'handle_after_sale_status']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render_orders_page(): void {
|
public function render_orders_page(): void {
|
||||||
@@ -42,20 +44,6 @@ class PayOrders {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'orders';
|
|
||||||
if (!in_array($tab, ['stats', 'orders', 'after-sales'], true)) $tab = 'orders';
|
|
||||||
$this->render_workbench_tabs($tab);
|
|
||||||
if ($tab === 'stats') {
|
|
||||||
$this->render_statistics_page();
|
|
||||||
$this->render_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ($tab === 'after-sales') {
|
|
||||||
$this->render_after_sales_page();
|
|
||||||
$this->render_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : '';
|
$detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : '';
|
||||||
if ($detail_order !== '') {
|
if ($detail_order !== '') {
|
||||||
$this->render_order_detail_page($detail_order);
|
$this->render_order_detail_page($detail_order);
|
||||||
@@ -89,7 +77,7 @@ class PayOrders {
|
|||||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
if (!array_key_exists($product_type, $type_tabs)) {
|
if (!array_key_exists($product_type, $type_tabs)) {
|
||||||
$product_type = '';
|
$product_type = '';
|
||||||
@@ -130,12 +118,17 @@ class PayOrders {
|
|||||||
? \AKSRT\Core\DomainModel::order_status_labels()
|
? \AKSRT\Core\DomainModel::order_status_labels()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Type tabs.
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
$tab_bits = [];
|
if ($page_num > $pages) {
|
||||||
foreach ($type_tabs as $tkey => $tlabel) {
|
$page_num = $pages;
|
||||||
$url = $this->page_url('aksrt_orders', array_filter([
|
$args['offset'] = ($page_num - 1) * $per_page;
|
||||||
'product_type' => $tkey !== '' ? $tkey : null,
|
$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,
|
'product_id' => $product_id > 0 ? $product_id : null,
|
||||||
'user_id' => $user_id > 0 ? $user_id : null,
|
'user_id' => $user_id > 0 ? $user_id : null,
|
||||||
'payment_method' => $pay_method !== '' ? $pay_method : null,
|
'payment_method' => $pay_method !== '' ? $pay_method : null,
|
||||||
@@ -143,7 +136,20 @@ class PayOrders {
|
|||||||
'date_from' => $date_from !== '' ? $date_from : null,
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
'date_to' => $date_to !== '' ? $date_to : null,
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
's' => $search !== '' ? $search : 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] + $query_args,
|
||||||
|
static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
}
|
||||||
|
));
|
||||||
$cls = ($product_type === $tkey) ? 'current' : '';
|
$cls = ($product_type === $tkey) ? 'current' : '';
|
||||||
$tab_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
|
$tab_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
|
||||||
}
|
}
|
||||||
@@ -154,16 +160,12 @@ class PayOrders {
|
|||||||
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
|
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
|
||||||
$m_bits = [];
|
$m_bits = [];
|
||||||
foreach ($method_tabs as $mkey => $mlabel) {
|
foreach ($method_tabs as $mkey => $mlabel) {
|
||||||
$url = $this->page_url('aksrt_orders', array_filter([
|
$url = $this->page_url('aksrt_orders', array_filter(
|
||||||
'product_type' => $product_type !== '' ? $product_type : null,
|
['payment_method' => $mkey !== '' ? $mkey : null] + $query_args,
|
||||||
'product_id' => $product_id > 0 ? $product_id : null,
|
static function ($value): bool {
|
||||||
'user_id' => $user_id > 0 ? $user_id : null,
|
return $value !== null;
|
||||||
'payment_method' => $mkey !== '' ? $mkey : null,
|
}
|
||||||
'status' => ($status !== '' && $status !== 'all') ? $status : null,
|
));
|
||||||
'date_from' => $date_from !== '' ? $date_from : null,
|
|
||||||
'date_to' => $date_to !== '' ? $date_to : null,
|
|
||||||
's' => $search !== '' ? $search : null,
|
|
||||||
]));
|
|
||||||
$cls = ($pay_method === $mkey) ? 'current' : '';
|
$cls = ($pay_method === $mkey) ? 'current' : '';
|
||||||
$m_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
|
$m_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
|
||||||
}
|
}
|
||||||
@@ -267,6 +269,15 @@ class PayOrders {
|
|||||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
|
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
|
||||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||||
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
||||||
|
// 已支付是已退款的唯一前置状态;退款仍需落到一条进行中的售后工单上。
|
||||||
|
if ($status_key === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||||
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认将该订单标记为已退款?不可恢复。', 'aksrt')) . '\');">';
|
||||||
|
echo '<input type="hidden" name="action" value="aksrt_pay_refund_order" />';
|
||||||
|
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||||
|
wp_nonce_field('aksrt_pay_refund_order_' . $order_number);
|
||||||
|
submit_button(__('退款', 'aksrt'), 'small', '', false);
|
||||||
|
echo '</form>';
|
||||||
|
}
|
||||||
$this->render_order_quick_status_buttons($order_number, $status_key);
|
$this->render_order_quick_status_buttons($order_number, $status_key);
|
||||||
if ($status_key === 'closed') {
|
if ($status_key === 'closed') {
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||||
@@ -282,19 +293,36 @@ class PayOrders {
|
|||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
|
||||||
if ($pages > 1) {
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
$this->render_footer();
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 售后服务子菜单(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();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +351,13 @@ class PayOrders {
|
|||||||
|
|
||||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||||
|
|
||||||
|
// 售后记录区块:工单卡片 + 处理记录列表(与售后面板共用 .aksrt-as-log 样式)。
|
||||||
|
echo '<style>'
|
||||||
|
. '.aksrt-as-ticket{padding:10px 0;border-bottom:1px solid #f0f0f1;}'
|
||||||
|
. '.aksrt-as-ticket:last-child{padding-bottom:0;border-bottom:0;}'
|
||||||
|
. '.aksrt-as-ticket__head{margin:0 0 4px;}'
|
||||||
|
. $this->after_sale_log_css()
|
||||||
|
. '</style>';
|
||||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||||
$rows = [
|
$rows = [
|
||||||
__('订单号', 'aksrt') => $order_number,
|
__('订单号', 'aksrt') => $order_number,
|
||||||
@@ -343,12 +378,17 @@ class PayOrders {
|
|||||||
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
||||||
if ($after_sales) {
|
if ($after_sales) {
|
||||||
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
||||||
$status_labels = ['pending' => '待审核', 'processing' => '处理中', 'approved' => '已通过', 'rejected' => '已拒绝', 'cancelled' => '已取消', 'completed' => '已完成'];
|
$status_labels = $this->after_sale_status_labels();
|
||||||
foreach ($after_sales as $application) {
|
foreach ($after_sales as $application) {
|
||||||
echo '<p><strong>' . esc_html($status_labels[(string) ($application['status'] ?? '')] ?? (string) ($application['status'] ?? '')) . '</strong> · ' . esc_html((string) ($application['refund_amount'] ?? '')) . '<br>' . esc_html((string) ($application['reason'] ?? ''));
|
$as_status = (string) ($application['status'] ?? '');
|
||||||
if (!empty($application['admin_note'])) echo '<br>' . esc_html__('处理备注:', 'aksrt') . esc_html((string) $application['admin_note']);
|
echo '<div class="aksrt-as-ticket is-' . esc_attr($as_status) . '">';
|
||||||
if (!empty($application['gateway_result'])) echo '<br><code>' . esc_html((string) $application['gateway_result']) . '</code>';
|
echo '<p class="aksrt-as-ticket__head"><strong>' . esc_html($status_labels[$as_status] ?? $as_status) . '</strong>'
|
||||||
echo '</p>';
|
. ' <span class="description">' . esc_html((string) ($application['refund_amount'] ?? '')) . '</span>'
|
||||||
|
. ' <span class="description">' . esc_html((string) ($application['created_at'] ?? '')) . '</span></p>';
|
||||||
|
echo '<p>' . nl2br(esc_html((string) ($application['reason'] ?? ''))) . '</p>';
|
||||||
|
// 处理记录:后台每一次提交都留一条(状态 + 备注 + 时间),与售后面板同款标记。
|
||||||
|
echo $this->render_after_sale_log((array) ($application['handling_log'] ?? []), $status_labels); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
echo '</div>';
|
||||||
}
|
}
|
||||||
echo '</td></tr>';
|
echo '</td></tr>';
|
||||||
}
|
}
|
||||||
@@ -372,90 +412,320 @@ class PayOrders {
|
|||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_workbench_tabs(string $active): void {
|
|
||||||
echo '<nav class="nav-tab-wrapper" style="margin:12px 0 18px;">';
|
|
||||||
foreach (['stats' => __('统计', 'aksrt'), 'orders' => __('订单', 'aksrt'), 'after-sales' => __('售后', 'aksrt')] as $key => $label) {
|
|
||||||
$url = $this->page_url(self::PAGE_SLUG, ['tab' => $key]);
|
|
||||||
echo '<a class="nav-tab ' . ($active === $key ? 'nav-tab-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
|
|
||||||
}
|
|
||||||
echo '</nav>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function render_statistics_page(): void {
|
|
||||||
$stats = \AKSRT\Payment\OrderRepository::get_instance()->get_admin_order_stats([
|
|
||||||
'date_from' => isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '',
|
|
||||||
'date_to' => isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '',
|
|
||||||
]);
|
|
||||||
echo '<form method="get" style="margin-bottom:16px;"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" /><input type="hidden" name="tab" value="stats" />';
|
|
||||||
echo '<label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr((string) ($_GET['date_from'] ?? '')) . '" /></label> ';
|
|
||||||
echo '<label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr((string) ($_GET['date_to'] ?? '')) . '" /></label> ';
|
|
||||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
|
||||||
echo '</form><div style="display:flex;flex-wrap:wrap;gap:12px;">';
|
|
||||||
foreach (['total' => __('订单总数', 'aksrt'), 'pending' => __('待支付', 'aksrt'), 'paid' => __('已支付', 'aksrt'), 'refunded' => __('已退款', 'aksrt'), 'closed' => __('已关闭', 'aksrt'), 'revenue' => __('实收金额', 'aksrt'), 'after_sale_pending' => __('售后待审核', 'aksrt')] as $key => $label) {
|
|
||||||
$this->stat_card($label, (string) ($stats[$key] ?? 0), $this->page_url(self::PAGE_SLUG, ['tab' => 'orders']));
|
|
||||||
}
|
|
||||||
echo '</div><h2>' . esc_html__('按商品类型统计', 'aksrt') . '</h2><table class="widefat striped" style="max-width:640px;"><tbody>';
|
|
||||||
foreach ((array) ($stats['by_type'] ?? []) as $type => $count) echo '<tr><th>' . esc_html(\AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type) . '</th><td>' . esc_html((string) $count) . '</td></tr>';
|
|
||||||
echo '</tbody></table>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function render_after_sales_page(): void {
|
private function render_after_sales_page(): void {
|
||||||
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
||||||
$status = isset($_GET['after_sale_status']) ? sanitize_key((string) wp_unslash($_GET['after_sale_status'])) : '';
|
$status = isset($_GET['after_sale_status']) ? sanitize_key((string) wp_unslash($_GET['after_sale_status'])) : '';
|
||||||
|
// admin_list() 会按状态常量白名单过滤,count() 不会。这里先归一一次,
|
||||||
|
// 否则手改 URL 传个野状态会变成「count 为 0 而 list 返回全部」,分页条直接说谎。
|
||||||
|
if (!in_array($status, $this->after_sale_statuses(), true)) {
|
||||||
|
$status = '';
|
||||||
|
}
|
||||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$date_from = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from) ? $date_from : '';
|
$date_from = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from) ? $date_from : '';
|
||||||
$date_to = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to) ? $date_to : '';
|
$date_to = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to) ? $date_to : '';
|
||||||
$filter_args = ['status' => $status, 'date_from' => $date_from, 'date_to' => $date_to];
|
$filter_args = ['status' => $status, 'date_from' => $date_from, 'date_to' => $date_to];
|
||||||
$items = $service->admin_list($filter_args + ['limit' => 100]);
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" /><input type="hidden" name="tab" value="after-sales" />';
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
|
$items = $service->admin_list($filter_args + ['limit' => $per_page, 'offset' => ($page_num - 1) * $per_page]);
|
||||||
|
$total = $service->count($filter_args);
|
||||||
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$items = $service->admin_list($filter_args + ['limit' => $per_page, 'offset' => ($page_num - 1) * $per_page]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_AFTER_SALES_SLUG) . '" />';
|
||||||
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
|
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
|
||||||
foreach ([\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成'] as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
foreach ($this->after_sale_status_labels() as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||||
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
|
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
|
||||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||||
echo '</form><table class="widefat striped"><thead><tr><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
|
echo '</form>';
|
||||||
if (!$items) echo '<tr><td colspan="7">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
echo '<style>'
|
||||||
|
. '.aksrt-as-detail-row>td{padding:0 0 6px;}'
|
||||||
|
. '.aksrt-as-detail-row[hidden]{display:none;}'
|
||||||
|
. '.aksrt-as-detail{margin:0 12px;padding:14px 18px;border:1px solid #dcdcde;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,.04);}'
|
||||||
|
. '.aksrt-as-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 28px;}'
|
||||||
|
. '.aksrt-as-field .aksrt-as-label{display:block;color:#646970;font-size:12px;margin-bottom:2px;}'
|
||||||
|
. '.aksrt-as-field .aksrt-as-value{display:block;word-break:break-word;}'
|
||||||
|
. '.aksrt-as-field{min-width:0;}'
|
||||||
|
. '.aksrt-as-thumb{width:48px;height:48px;object-fit:cover;border-radius:4px;margin:0 4px 4px 0;vertical-align:middle;}'
|
||||||
|
. '.aksrt-as-detail .aksrt-as-thumb{width:80px;height:80px;}'
|
||||||
|
. '.aksrt-as-review{margin-top:14px;padding-top:12px;border-top:1px solid #dcdcde;display:flex;flex-wrap:wrap;align-items:flex-end;gap:12px 16px;}'
|
||||||
|
. '.aksrt-as-review-head{flex:1 1 100%;}'
|
||||||
|
. '.aksrt-as-review-main{flex:1 1 460px;min-width:0;display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 12px;}'
|
||||||
|
. '.aksrt-as-review-main p{margin:0;}'
|
||||||
|
. '.aksrt-status-select{min-width:150px;}'
|
||||||
|
. '.aksrt-as-refund-flag{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;margin:0;}'
|
||||||
|
. '.aksrt-as-review textarea{flex:1 1 240px;min-width:0;box-sizing:border-box;margin:0;}'
|
||||||
|
. '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;}'
|
||||||
|
. '.aksrt-as-field--wide{grid-column:1/-1;}'
|
||||||
|
. $this->after_sale_log_css()
|
||||||
|
. '@media (min-width:1500px){.aksrt-as-detail-grid{grid-template-columns:repeat(5,minmax(0,1fr));}}'
|
||||||
|
. '@media (max-width:600px){.aksrt-as-detail-grid{grid-template-columns:minmax(0,1fr);}}'
|
||||||
|
. '</style>';
|
||||||
|
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>申请资料</th><th>申请说明</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
|
||||||
|
if (!$items) echo '<tr><td colspan="9">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
||||||
|
$status_labels = $this->after_sale_status_labels();
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
||||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($number);
|
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($number);
|
||||||
$type = $order ? \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? [])) : '';
|
$type = $order ? \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? [])) : '';
|
||||||
$type_label = \AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type;
|
$type_label = \AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type;
|
||||||
$status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成'];
|
$product_name = $order ? (string) (\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '';
|
||||||
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
$refund_amount = (string) ($item['refund_amount'] ?? '0');
|
||||||
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
|
if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount;
|
||||||
echo '<tr><td>' . esc_html((string) $id) . '<br><small>' . esc_html((string) ($item['created_at'] ?? '')) . '</small></td><td><a href="' . esc_url($this->page_url(self::PAGE_SLUG, ['tab' => 'orders', 'order' => $number])) . '"><code>' . esc_html($number) . '</code></a><br><span class="description">' . esc_html($type_label) . ($order ? ' · ' . esc_html(\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '') . '</span></td><td>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '</td><td>' . esc_html((string) $item['reason']) . '</td><td>' . esc_html($refund_amount) . '</td><td>' . esc_html($status_labels[(string) $item['status']] ?? (string) $item['status']) . '</td><td>';
|
$apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null;
|
||||||
if ((string) $item['status'] === 'pending') foreach (['approve' => '通过', 'reject' => '拒绝'] as $decision => $label) {
|
if ($apply_form) {
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;vertical-align:top;margin-right:4px;"><input type="hidden" name="action" value="aksrt_after_sale_review" /><input type="hidden" name="after_sale_id" value="' . esc_attr((string) $id) . '" /><input type="hidden" name="decision" value="' . esc_attr($decision) . '" />'; wp_nonce_field('aksrt_after_sale_review_' . $id); echo '<textarea name="admin_note" rows="2" placeholder="' . esc_attr__('管理员备注', 'aksrt') . '"></textarea><br><button class="button button-small" type="submit">' . esc_html($label) . '</button></form>';
|
$reason_cell = esc_html((string) ($apply_form['reason_label'] ?? ''));
|
||||||
|
$material_lines = [];
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
if ((string) $item['status'] === 'processing') {
|
$materials_cell = $material_lines ? implode('<br>', $material_lines) : '—';
|
||||||
$gateway_result = json_decode((string) ($item['gateway_result'] ?? ''), true);
|
$desc_text = trim((string) ($apply_form['description'] ?? ''));
|
||||||
$manual_refund = is_array($gateway_result) && sanitize_key((string) ($gateway_result['status'] ?? '')) === 'manual';
|
} else {
|
||||||
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>';
|
$reason_cell = nl2br(esc_html((string) $item['reason']));
|
||||||
|
$materials_cell = '—';
|
||||||
|
$desc_text = '';
|
||||||
}
|
}
|
||||||
|
$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>';
|
||||||
}
|
}
|
||||||
if (!empty($item['admin_note'])) echo '<small>' . esc_html__('备注:', 'aksrt') . esc_html((string) $item['admin_note']) . '</small><br>';
|
// 举证图片并入申请说明:缩略图展示在描述文字下方,不再单独成列。
|
||||||
if (!empty($item['gateway_result'])) {
|
$desc_cell = ($desc_text !== '' ? nl2br(esc_html($desc_text)) : '') . ($thumbs ? '<div class="aksrt-as-desc-thumbs" style="margin-top:6px;">' . implode('', $thumbs) . '</div>' : '');
|
||||||
$result_data = json_decode((string) $item['gateway_result'], true);
|
if ($desc_cell === '') $desc_cell = '—';
|
||||||
$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'];
|
$status_label = $status_labels[(string) $item['status']] ?? (string) $item['status'];
|
||||||
echo '<small>' . esc_html__('渠道结果:', 'aksrt') . ' ' . esc_html($result_text) . '</small>';
|
$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 '</td></tr>';
|
// 处理备注:单独占整行,放在申请说明上方;每次提交一条,含状态与时间。
|
||||||
|
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('处理备注', 'aksrt') . '</span><span class="aksrt-as-value">'
|
||||||
|
. $this->render_after_sale_log((array) ($item['handling_log'] ?? []), $status_labels) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
. '</span></span>';
|
||||||
|
// 举证图片并入申请说明,单独占整行展示。
|
||||||
|
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('申请说明', 'aksrt') . '</span><span class="aksrt-as-value">' . $desc_cell . '</span></span>';
|
||||||
|
echo '</div>';
|
||||||
|
$status_options = '';
|
||||||
|
$current_status = (string) $item['status'];
|
||||||
|
foreach ($status_labels as $key => $label) {
|
||||||
|
$status_options .= '<option value="' . esc_attr($key) . '"' . selected($current_status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||||
|
}
|
||||||
|
if (!isset($status_labels[$current_status])) {
|
||||||
|
// 历史状态(如已删除的 approved)不在可选项内:如实呈现且禁用,防止误提交。
|
||||||
|
$status_options .= '<option value="' . esc_attr($current_status) . '" selected="selected" disabled>' . esc_html($current_status) . '</option>';
|
||||||
|
}
|
||||||
|
echo '<div class="aksrt-as-review">';
|
||||||
|
echo '<p class="aksrt-as-review-head" style="margin:0;"><strong>' . esc_html__('售后状态', 'aksrt') . '</strong> <span class="description">' . esc_html__('选择目标状态后点击确认;取消仅收起面板。', 'aksrt') . '</span></p>';
|
||||||
|
echo '<div class="aksrt-as-review-main">';
|
||||||
|
echo '<p><select class="aksrt-status-select">' . $status_options . '</select></p>';
|
||||||
|
echo '<textarea class="aksrt-review-note" rows="2" placeholder="' . esc_attr__('填写处理备注(可留空)', 'aksrt') . '"></textarea>';
|
||||||
|
echo '</div>';
|
||||||
|
// 勾选后在确认时把关联订单推到已退款;订单本已退款时呈现为不可改的已选状态
|
||||||
|
echo '<label class="aksrt-as-refund-flag"><input type="checkbox" class="aksrt-mark-order-refunded" value="1"'
|
||||||
|
. ($order_refunded ? ' checked="checked" disabled' : '')
|
||||||
|
. ' /> ' . esc_html__('标记订单已退款', 'aksrt') . '</label>';
|
||||||
|
echo '<div class="aksrt-as-review-actions">';
|
||||||
|
echo '<button type="button" class="button aksrt-after-sale-cancel">' . esc_html__('取消', 'aksrt') . '</button>';
|
||||||
|
echo '<button type="button" class="button button-primary aksrt-after-sale-confirm"'
|
||||||
|
. ' data-id="' . esc_attr((string) $id) . '"'
|
||||||
|
. ' data-nonce="' . esc_attr(wp_create_nonce('aksrt_after_sale_status_' . $id)) . '">'
|
||||||
|
. esc_html__('确认', 'aksrt') . '</button>';
|
||||||
|
echo '</div></div>';
|
||||||
|
echo '</div></td></tr>';
|
||||||
}
|
}
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'after_sale_status' => $status !== '' ? $status : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
AdminListPagination::render(self::PAGE_AFTER_SALES_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
|
|
||||||
|
// 事件委托:点击「操作」切换手风琴详情;「取消」仅收起面板;「确认」以 POST 提交选中的售后状态
|
||||||
|
echo '<script>(function(){'
|
||||||
|
. 'var postUrl=' . wp_json_encode(esc_url_raw(admin_url('admin-post.php'))) . ';'
|
||||||
|
. 'var tConfirmStatus=' . wp_json_encode(__('确认将售后状态设置为', 'aksrt')) . ';'
|
||||||
|
. 'function closeAllDetails(){'
|
||||||
|
. 'document.querySelectorAll(".aksrt-as-detail-row").forEach(function(x){x.setAttribute("hidden","");});'
|
||||||
|
. 'document.querySelectorAll(".aksrt-toggle-row-actions").forEach(function(t){t.setAttribute("aria-expanded","false");});'
|
||||||
|
. '}'
|
||||||
|
. 'document.addEventListener("click",function(e){'
|
||||||
|
. 'var el=e.target;'
|
||||||
|
. 'var t=el&&el.closest?el.closest(".aksrt-toggle-row-actions"):null;'
|
||||||
|
. 'if(t){e.preventDefault();'
|
||||||
|
. 'var p=document.getElementById(t.getAttribute("data-target"));if(!p)return;'
|
||||||
|
. 'var willOpen=p.hasAttribute("hidden");closeAllDetails();'
|
||||||
|
. 'if(willOpen){p.removeAttribute("hidden");t.setAttribute("aria-expanded","true");'
|
||||||
|
. 'var s=p.querySelector(".aksrt-status-select");if(s)s.focus();}'
|
||||||
|
. 'return;}'
|
||||||
|
. 'var c=el&&el.closest?el.closest(".aksrt-after-sale-cancel"):null;'
|
||||||
|
. 'if(c){e.preventDefault();closeAllDetails();return;}'
|
||||||
|
. 'var a=el&&el.closest?el.closest(".aksrt-after-sale-confirm"):null;'
|
||||||
|
. 'if(!a)return;e.preventDefault();'
|
||||||
|
. 'var panel=a.closest(".aksrt-as-detail-row");'
|
||||||
|
. 'var sel=panel?panel.querySelector(".aksrt-status-select"):null;'
|
||||||
|
. 'var noteEl=panel?panel.querySelector(".aksrt-review-note"):null;'
|
||||||
|
. 'var refundEl=panel?panel.querySelector(".aksrt-mark-order-refunded"):null;'
|
||||||
|
. 'var markRefund=!!(refundEl&&refundEl.checked&&!refundEl.disabled);'
|
||||||
|
. 'var opt=sel?sel.options[sel.selectedIndex]:null;'
|
||||||
|
. 'if(!sel||!opt||opt.disabled){alert(' . wp_json_encode(__('请选择售后状态。', 'aksrt')) . ');return;}'
|
||||||
|
. 'var confirmText=tConfirmStatus+"「"+opt.text+"」?";'
|
||||||
|
. 'if(markRefund)confirmText+=' . wp_json_encode(__('同时会将关联订单标记为已退款。', 'aksrt')) . ';'
|
||||||
|
. 'if(!confirm(confirmText))return;'
|
||||||
|
. 'var f=document.createElement("form");f.method="post";f.action=postUrl;'
|
||||||
|
. 'var fields={action:"aksrt_after_sale_update",after_sale_id:a.getAttribute("data-id"),after_sale_status:sel.value,mark_order_refunded:markRefund?"1":"",_wpnonce:a.getAttribute("data-nonce"),admin_note:noteEl?noteEl.value:""};'
|
||||||
|
. 'Object.keys(fields).forEach(function(k){var i=document.createElement("input");i.type="hidden";i.name=k;i.value=fields[k];f.appendChild(i);});'
|
||||||
|
. 'document.body.appendChild(f);f.submit();'
|
||||||
|
. '});'
|
||||||
|
. '})();</script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_after_sale_review(): void {
|
/** 售后状态白名单:admin_list() 据此过滤,count() 不校验,故查询前先按它归一。 */
|
||||||
|
private function after_sale_statuses(): array {
|
||||||
|
return [
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PENDING,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED,
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 售后状态中文标签:订单明细页与售后面板(含筛选下拉)共用,取代两处内联数组。 */
|
||||||
|
/** 处理记录列表样式:两个后台页面各自输出同一套规则,不依赖对方加载。 */
|
||||||
|
private function after_sale_log_css(): string {
|
||||||
|
return '.aksrt-as-log{display:grid;gap:5px;margin:6px 0 0;padding:0;list-style:none;}'
|
||||||
|
. '.aksrt-as-log__item{display:flex;flex-wrap:wrap;gap:3px 10px;align-items:baseline;font-size:12px;line-height:1.7;}'
|
||||||
|
. '.aksrt-as-log__time{color:#646970;font-variant-numeric:tabular-nums;}'
|
||||||
|
. '.aksrt-as-log__status{font-weight:600;}'
|
||||||
|
. '.aksrt-as-log__item.is-pending .aksrt-as-log__status{color:#b26200;}'
|
||||||
|
. '.aksrt-as-log__item.is-processing .aksrt-as-log__status{color:#2271b1;}'
|
||||||
|
. '.aksrt-as-log__item.is-completed .aksrt-as-log__status{color:#007017;}'
|
||||||
|
. '.aksrt-as-log__item.is-rejected .aksrt-as-log__status,.aksrt-as-log__item.is-cancelled .aksrt-as-log__status{color:#b32d2e;}'
|
||||||
|
. '.aksrt-as-log__note{flex:1 1 100%;color:#3c434a;}';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function after_sale_status_labels(): array {
|
||||||
|
return [
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝',
|
||||||
|
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理记录列表:每一次状态提交一条「时间 + 状态 + 备注」,备注为空也留痕。
|
||||||
|
* 后台订单明细页与售后面板共用同一套标记与样式。
|
||||||
|
*
|
||||||
|
* @param list<array<string,mixed>> $log
|
||||||
|
* @param array<string,string> $status_labels
|
||||||
|
*/
|
||||||
|
private function render_after_sale_log(array $log, array $status_labels): string {
|
||||||
|
if (!$log) {
|
||||||
|
return '<p class="description">' . esc_html__('暂无处理记录', 'aksrt') . '</p>';
|
||||||
|
}
|
||||||
|
$service = \AKSRT\Payment\AfterSaleService::class;
|
||||||
|
$html = '<ul class="aksrt-as-log">';
|
||||||
|
foreach ($log as $entry) {
|
||||||
|
$status = (string) ($entry['status'] ?? '');
|
||||||
|
$label = (string) ($entry['source'] ?? '') === $service::SOURCE_USER
|
||||||
|
? ($status === $service::STATUS_CANCELLED ? esc_html__('用户撤销售后', 'aksrt') : esc_html__('用户提交申请', 'aksrt'))
|
||||||
|
: ($status_labels[$status] ?? $status);
|
||||||
|
$note = (string) ($entry['note'] ?? '');
|
||||||
|
$html .= '<li class="aksrt-as-log__item is-' . esc_attr($status) . '">'
|
||||||
|
. '<span class="aksrt-as-log__time">' . esc_html((string) ($entry['created_at'] ?? '')) . '</span>'
|
||||||
|
. '<span class="aksrt-as-log__status">' . esc_html($label) . '</span>'
|
||||||
|
. '<span class="aksrt-as-log__note">' . ($note !== '' ? nl2br(esc_html($note)) : '—') . '</span>'
|
||||||
|
. '</li>';
|
||||||
|
}
|
||||||
|
return $html . '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle_after_sale_status(): void {
|
||||||
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
||||||
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
||||||
check_admin_referer('aksrt_after_sale_review_' . $id);
|
check_admin_referer('aksrt_after_sale_status_' . $id);
|
||||||
$decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
|
$status = isset($_POST['after_sale_status']) ? sanitize_key((string) wp_unslash($_POST['after_sale_status'])) : '';
|
||||||
$note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
|
$note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
|
||||||
$result = \AKSRT\Payment\AfterSaleService::get_instance()->review($id, $decision, $note);
|
$mark_refund = isset($_POST['mark_order_refunded']) && (string) wp_unslash($_POST['mark_order_refunded']) === '1';
|
||||||
$args = ['tab' => 'after-sales', 'aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_review_failed' : 'after_sale_reviewed'];
|
|
||||||
|
// 勾选「标记订单已退款」时先落订单状态:售后工单一旦离开 pending/processing,
|
||||||
|
// OrderRepository 的退款闸门就不再放行,所以顺序不能反。
|
||||||
|
if ($mark_refund && $this->mark_related_order_refunded($id) === 'failed') {
|
||||||
|
wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, ['aksrt_pay_msg' => 'after_sale_order_refund_failed']));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$result = \AKSRT\Payment\AfterSaleService::get_instance()->set_status($id, $status, $note);
|
||||||
|
$args = ['aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_update_failed' : 'after_sale_updated'];
|
||||||
if (is_wp_error($result)) $args['after_sale_error'] = substr($result->get_error_message(), 0, 240);
|
if (is_wp_error($result)) $args['after_sale_error'] = substr($result->get_error_message(), 0, 240);
|
||||||
wp_safe_redirect($this->page_url(self::PAGE_SLUG, $args)); exit;
|
elseif ($mark_refund) $args['aksrt_pay_msg'] = 'after_sale_updated_order_refunded';
|
||||||
|
wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, $args)); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把售后申请关联的订单推到已退款。仅在该工单仍为待审核/处理中时放行,
|
||||||
|
* 避免对已关闭的工单或非已支付订单误标退款。
|
||||||
|
*
|
||||||
|
* @return string 'done' | 'failed'
|
||||||
|
*/
|
||||||
|
private function mark_related_order_refunded(int $after_sale_id): string {
|
||||||
|
if (!class_exists('\AKSRT\Payment\AfterSaleService') || !class_exists('\AKSRT\Payment\OrderRepository')) return 'failed';
|
||||||
|
$service = \AKSRT\Payment\AfterSaleService::get_instance();
|
||||||
|
$row = $service->get($after_sale_id);
|
||||||
|
$order_number = is_array($row) ? (string) ($row['order_number'] ?? '') : '';
|
||||||
|
if ($order_number === '' || !$service->can_finalize_order_refund($after_sale_id, $order_number)) return 'failed';
|
||||||
|
|
||||||
|
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||||
|
$order = $repo->get($order_number);
|
||||||
|
if (is_array($order) && (string) ($order['status'] ?? '') === \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED) return 'done';
|
||||||
|
return $repo->update_status($order_number, \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED, [
|
||||||
|
'allow_refund' => true,
|
||||||
|
'after_sale_id' => $after_sale_id,
|
||||||
|
]) ? 'done' : 'failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_update_order_status(): void {
|
public function handle_update_order_status(): void {
|
||||||
@@ -482,6 +752,47 @@ class PayOrders {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单列表「退款」:把已支付订单推到已退款。退款必须落到一条进行中的售后工单上,
|
||||||
|
* 工单号由服务端按订单号反查,不经客户端传递。
|
||||||
|
*/
|
||||||
|
public function handle_refund_order(): void {
|
||||||
|
if (!current_user_can(self::CAPABILITY)) {
|
||||||
|
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
|
||||||
|
check_admin_referer('aksrt_pay_refund_order_' . $order_number);
|
||||||
|
|
||||||
|
$code = 'order_refund_failed';
|
||||||
|
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
|
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||||
|
$order = $repo->get($order_number);
|
||||||
|
if (!is_array($order)) {
|
||||||
|
$code = 'order_refund_missing';
|
||||||
|
} elseif ((string) ($order['status'] ?? '') !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||||
|
$code = 'order_refund_not_paid';
|
||||||
|
} else {
|
||||||
|
$ticket = class_exists('\AKSRT\Payment\AfterSaleService')
|
||||||
|
? \AKSRT\Payment\AfterSaleService::get_instance()->find_active($order_number)
|
||||||
|
: null;
|
||||||
|
if (!is_array($ticket)) {
|
||||||
|
$code = 'order_refund_no_ticket';
|
||||||
|
} else {
|
||||||
|
$ok = $repo->update_status($order_number, \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED, [
|
||||||
|
'allow_refund' => true,
|
||||||
|
'after_sale_id' => (int) ($ticket['id'] ?? 0),
|
||||||
|
'note' => __('管理员在订单列表中标记退款', 'aksrt'),
|
||||||
|
]);
|
||||||
|
$code = $ok ? 'order_refunded' : 'order_refund_failed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_safe_redirect($this->page_url('aksrt_orders', ['aksrt_pay_msg' => $code]));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
public function handle_update_order_note(): void {
|
public function handle_update_order_note(): void {
|
||||||
if (!current_user_can(self::CAPABILITY)) {
|
if (!current_user_can(self::CAPABILITY)) {
|
||||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||||
@@ -570,8 +881,6 @@ class PayOrders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function ensure_cap(): void {
|
private function ensure_cap(): void {
|
||||||
if (!current_user_can(self::CAPABILITY)) {
|
if (!current_user_can(self::CAPABILITY)) {
|
||||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||||
@@ -622,6 +931,11 @@ class PayOrders {
|
|||||||
$map = [
|
$map = [
|
||||||
'order_updated' => __('订单状态已更新。', 'aksrt'),
|
'order_updated' => __('订单状态已更新。', 'aksrt'),
|
||||||
'order_failed' => __('订单状态更新失败。', 'aksrt'),
|
'order_failed' => __('订单状态更新失败。', 'aksrt'),
|
||||||
|
'order_refunded' => __('订单已标记为已退款。', 'aksrt'),
|
||||||
|
'order_refund_failed' => __('订单标记退款失败。', 'aksrt'),
|
||||||
|
'order_refund_missing' => __('订单不存在。', 'aksrt'),
|
||||||
|
'order_refund_not_paid' => __('仅已支付订单可以标记退款。', 'aksrt'),
|
||||||
|
'order_refund_no_ticket' => __('该订单没有进行中的售后工单,无法标记退款;请先在「售后服务」中处理。', 'aksrt'),
|
||||||
'order_note_updated' => __('订单备注已保存。', 'aksrt'),
|
'order_note_updated' => __('订单备注已保存。', 'aksrt'),
|
||||||
'order_note_failed' => __('订单备注保存失败。', 'aksrt'),
|
'order_note_failed' => __('订单备注保存失败。', 'aksrt'),
|
||||||
'orders_cleared' => sprintf(
|
'orders_cleared' => sprintf(
|
||||||
@@ -638,15 +952,17 @@ class PayOrders {
|
|||||||
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
|
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
|
||||||
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
|
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
|
||||||
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
|
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
|
||||||
'after_sale_reviewed' => __('售后审核已处理。', 'aksrt'),
|
'after_sale_updated' => __('售后状态已更新。', 'aksrt'),
|
||||||
'after_sale_review_failed' => __('售后审核失败,请查看申请状态和错误原因。', 'aksrt'),
|
'after_sale_updated_order_refunded' => __('售后状态已更新,关联订单已标记为已退款。', 'aksrt'),
|
||||||
|
'after_sale_order_refund_failed' => __('关联订单标记已退款失败:需售后工单仍为待审核/处理中,且订单为已支付。', 'aksrt'),
|
||||||
|
'after_sale_update_failed' => __('售后状态更新失败,请查看申请状态和错误原因。', 'aksrt'),
|
||||||
];
|
];
|
||||||
if (!isset($map[$code])) {
|
if (!isset($map[$code])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||||
if ($code === 'after_sale_review_failed' && isset($_GET['after_sale_error'])) {
|
if ($code === 'after_sale_update_failed' && isset($_GET['after_sale_error'])) {
|
||||||
echo '<div class="notice notice-error"><p>' . esc_html(sanitize_text_field((string) wp_unslash($_GET['after_sale_error']))) . '</p></div>';
|
echo '<div class="notice notice-error"><p>' . esc_html(sanitize_text_field((string) wp_unslash($_GET['after_sale_error']))) . '</p></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,27 +970,4 @@ class PayOrders {
|
|||||||
private function page_url(string $slug, array $args = []): string {
|
private function page_url(string $slug, array $args = []): string {
|
||||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stat_card(string $label, string $value, string $url): void {
|
|
||||||
echo '<a href="' . esc_url($url) . '" style="display:block;min-width:140px;padding:16px;background:#fff;border:1px solid #c3c4c7;border-radius:4px;text-decoration:none;color:inherit;">';
|
|
||||||
echo '<div style="font-size:12px;color:#646970;">' . esc_html($label) . '</div>';
|
|
||||||
echo '<div style="font-size:22px;font-weight:600;margin-top:6px;">' . esc_html($value) . '</div>';
|
|
||||||
echo '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ class PayProducts {
|
|||||||
$pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
|
$pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
|
||||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||||
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
||||||
@@ -51,9 +51,22 @@ class PayProducts {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$posts = $query['posts'];
|
$posts = $query['posts'];
|
||||||
$total = $query['total'];
|
$total = (int) $query['total'];
|
||||||
$pages = max(1, (int) ceil($total / $per_page));
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 WP_Query 停在空页上:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$query = $this->query_paid_posts([
|
||||||
|
'pay_type' => $pay_type,
|
||||||
|
'search' => $search,
|
||||||
|
'page' => $page_num,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
]);
|
||||||
|
$posts = $query['posts'];
|
||||||
|
}
|
||||||
|
|
||||||
$sales_map = [];
|
$sales_map = [];
|
||||||
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
|
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
$ids = array_map(static function ($p) {
|
$ids = array_map(static function ($p) {
|
||||||
@@ -138,10 +151,6 @@ class PayProducts {
|
|||||||
echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
||||||
}
|
}
|
||||||
echo '<a class="button button-small" href="' . esc_url($orders_url) . '">' . esc_html__('订单', 'aksrt') . '</a>';
|
echo '<a class="button button-small" href="' . esc_url($orders_url) . '">' . esc_html__('订单', 'aksrt') . '</a>';
|
||||||
if ($pt === \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
|
|
||||||
$dl_url = $this->page_url('aksrt_download_logs', ['post_id' => $post_id]);
|
|
||||||
echo '<a class="button button-small" href="' . esc_url($dl_url) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
|
|
||||||
}
|
|
||||||
$permalink = get_permalink($post_id);
|
$permalink = get_permalink($post_id);
|
||||||
if ($permalink) {
|
if ($permalink) {
|
||||||
echo '<a class="button button-small" target="_blank" rel="noopener noreferrer" href="' . esc_url($permalink) . '">' . esc_html__('查看', 'aksrt') . '</a>';
|
echo '<a class="button button-small" target="_blank" rel="noopener noreferrer" href="' . esc_url($permalink) . '">' . esc_html__('查看', 'aksrt') . '</a>';
|
||||||
@@ -150,8 +159,16 @@ class PayProducts {
|
|||||||
echo '</tr>';
|
echo '</tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与 render_filters() 的表单一致。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'pay_type' => $pay_type !== '' ? $pay_type : null,
|
||||||
|
's' => $search !== '' ? $search : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
$this->render_pagination($pages, $page_num);
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,20 +373,4 @@ class PayProducts {
|
|||||||
private function page_url(string $slug, array $args = []): string {
|
private function page_url(string $slug, array $args = []): string {
|
||||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ class PayWithdrawals {
|
|||||||
$date_from = isset($_GET['date_from']) ? $this->sanitize_date((string) wp_unslash($_GET['date_from'])) : '';
|
$date_from = isset($_GET['date_from']) ? $this->sanitize_date((string) wp_unslash($_GET['date_from'])) : '';
|
||||||
$date_to = isset($_GET['date_to']) ? $this->sanitize_date((string) wp_unslash($_GET['date_to'])) : '';
|
$date_to = isset($_GET['date_to']) ? $this->sanitize_date((string) wp_unslash($_GET['date_to'])) : '';
|
||||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||||
$per_page = 20;
|
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
|
||||||
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
||||||
$stats = $svc->get_stats();
|
$stats = $svc->get_stats();
|
||||||
$retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0;
|
$retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0;
|
||||||
@@ -83,7 +83,7 @@ class PayWithdrawals {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
echo '</select> ';
|
echo '</select> ';
|
||||||
echo '<input type="search" name="s" value="' . esc_attr($keyword) . '" placeholder="' . esc_attr__('流水号 / 用户 ID', 'aksrt') . '" /> ';
|
echo '<input type="search" name="s" value="' . esc_attr($keyword) . '" placeholder="' . esc_attr__('订单号 / 用户 ID', 'aksrt') . '" /> ';
|
||||||
echo '<input type="number" min="1" name="user_id" value="' . esc_attr($user_filter > 0 ? (string) $user_filter : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
|
echo '<input type="number" min="1" name="user_id" value="' . esc_attr($user_filter > 0 ? (string) $user_filter : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
|
||||||
echo '<label>' . esc_html__('从', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '"></label> ';
|
echo '<label>' . esc_html__('从', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '"></label> ';
|
||||||
echo '<label>' . esc_html__('至', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '"></label> ';
|
echo '<label>' . esc_html__('至', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '"></label> ';
|
||||||
@@ -111,9 +111,36 @@ class PayWithdrawals {
|
|||||||
]);
|
]);
|
||||||
$pages = max(1, (int) ceil($total / $per_page));
|
$pages = max(1, (int) ceil($total / $per_page));
|
||||||
|
|
||||||
|
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||||||
|
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
|
||||||
|
if ($page_num > $pages) {
|
||||||
|
$page_num = $pages;
|
||||||
|
$items = $svc->get_all([
|
||||||
|
'status' => $status,
|
||||||
|
'keyword' => $keyword,
|
||||||
|
'user_id' => $user_filter,
|
||||||
|
'date_from' => $date_from,
|
||||||
|
'date_to' => $date_to,
|
||||||
|
'limit' => $per_page,
|
||||||
|
'offset' => ($page_num - 1) * $per_page,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||||
|
// retry_id / retry_key 是一次性动作参数,绝不能进翻页链接。
|
||||||
|
$query_args = array_filter([
|
||||||
|
'status' => $status !== '' ? $status : null,
|
||||||
|
's' => $keyword !== '' ? $keyword : null,
|
||||||
|
'user_id' => $user_filter > 0 ? $user_filter : null,
|
||||||
|
'date_from' => $date_from !== '' ? $date_from : null,
|
||||||
|
'date_to' => $date_to !== '' ? $date_to : null,
|
||||||
|
], static function ($value): bool {
|
||||||
|
return $value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
echo '<table class="wp-list-table widefat fixed striped">';
|
echo '<table class="wp-list-table widefat fixed striped">';
|
||||||
echo '<thead><tr>';
|
echo '<thead><tr>';
|
||||||
echo '<th>' . esc_html__('流水号', 'aksrt') . '</th>';
|
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
||||||
echo '<th>' . esc_html__('金额明细', 'aksrt') . '</th>';
|
echo '<th>' . esc_html__('金额明细', 'aksrt') . '</th>';
|
||||||
echo '<th>' . esc_html__('渠道 / 账户', 'aksrt') . '</th>';
|
echo '<th>' . esc_html__('渠道 / 账户', 'aksrt') . '</th>';
|
||||||
@@ -184,18 +211,7 @@ class PayWithdrawals {
|
|||||||
|
|
||||||
echo '</tbody></table>';
|
echo '</tbody></table>';
|
||||||
|
|
||||||
if ($pages > 1) {
|
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->render_footer();
|
$this->render_footer();
|
||||||
}
|
}
|
||||||
@@ -278,19 +294,4 @@ class PayWithdrawals {
|
|||||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1 ? $date : '';
|
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1 ? $date : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render_pagination(int $pages, int $page_num): void {
|
|
||||||
if ($pages <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
||||||
echo paginate_links([
|
|
||||||
'base' => add_query_arg('paged', '%#%'),
|
|
||||||
'format' => '',
|
|
||||||
'prev_text' => '«',
|
|
||||||
'next_text' => '»',
|
|
||||||
'total' => $pages,
|
|
||||||
'current' => $page_num,
|
|
||||||
]);
|
|
||||||
echo '</div></div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Quick-edit field definitions for the admin post list.
|
||||||
|
*
|
||||||
|
* Column registration, rendering and saving live in PostListEnhance; this file
|
||||||
|
* only describes the field groups so the two concerns stay separately readable.
|
||||||
|
* Every key here must exist in DomainModel::default_pay_meta(),
|
||||||
|
* DomainModel::default_post_meta() or the custom-filter settings — the
|
||||||
|
* post-list-quick-edit contract test enforces that pairing.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin\Native;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class PostListEditFields {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interaction counts + article display config (saved to post meta).
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function extended(): array {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'id' => 'views',
|
||||||
|
'type' => 'number',
|
||||||
|
'title' => __('阅读量', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'like',
|
||||||
|
'type' => 'number',
|
||||||
|
'title' => __('点赞数', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'show_layout',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('显示布局', 'aksrt'),
|
||||||
|
'default' => 'false',
|
||||||
|
'options' => [
|
||||||
|
'false' => __('跟随主题', 'aksrt'),
|
||||||
|
'no_sidebar' => __('无侧边栏', 'aksrt'),
|
||||||
|
'sidebar_left' => __('侧边栏靠左', 'aksrt'),
|
||||||
|
'sidebar_right' => __('侧边栏靠右', 'aksrt'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'no_article-navs',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('不显示目录树', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'article_maxheight_xz',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('限制内容最大高度', 'aksrt'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pay parameters (saved into the _aksrt_pay meta bag).
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function pay(string $currency): array {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'id' => 'pay_type',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('付费模式', 'aksrt'),
|
||||||
|
'options' => \AKSRT\Core\DomainModel::pay_type_labels(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'pay_limit',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('购买权限', 'aksrt'),
|
||||||
|
'options' => [
|
||||||
|
'0' => __('所有人可购买', 'aksrt'),
|
||||||
|
'1' => __('VIP1 及以上会员可购买', 'aksrt'),
|
||||||
|
'2' => __('仅 VIP2 可购买', 'aksrt'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'pay_modo',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('支付类型', 'aksrt'),
|
||||||
|
'options' => [
|
||||||
|
'0' => __('普通商品(金钱购买)', 'aksrt'),
|
||||||
|
'points' => __('积分商品(积分购买,依赖于用户积分功能)', 'aksrt'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
|
||||||
|
'id' => 'points_price',
|
||||||
|
'title' => __('积分售价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => __('积分', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
|
||||||
|
'title' => __('VIP1 积分售价', 'aksrt'),
|
||||||
|
'id' => 'vip1_points',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP1 免费', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => __('积分', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
|
||||||
|
'title' => __('VIP2 积分售价', 'aksrt'),
|
||||||
|
'id' => 'vip2_points',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP2 免费', 'aksrt'),
|
||||||
|
'desc' => __('会员价格不能高于售价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => __('积分', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'id' => 'price',
|
||||||
|
'title' => __('执行价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'id' => 'original_price',
|
||||||
|
'title' => __('原价', 'aksrt'),
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('显示在执行价格前面,并划掉', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'title' => __('VIP1 价格', 'aksrt'),
|
||||||
|
'id' => 'vip1_price',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP1 免费', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'title' => __('VIP2 价格', 'aksrt'),
|
||||||
|
'id' => 'vip2_price',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP2 免费', 'aksrt'),
|
||||||
|
'desc' => __('会员价格不能高于执行价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'pay_count',
|
||||||
|
'title' => __('销量浮动', 'aksrt'),
|
||||||
|
'subtitle' => __('为真实销量增加或减少的数量', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'show_sales',
|
||||||
|
'title' => __('显示销量', 'aksrt'),
|
||||||
|
'default' => true,
|
||||||
|
'type' => 'switcher',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'coupon_s',
|
||||||
|
'title' => __('允许使用优惠码', 'aksrt'),
|
||||||
|
'default' => false,
|
||||||
|
'type' => 'switcher',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'order_expired_s',
|
||||||
|
'title' => __('订单时效', 'aksrt'),
|
||||||
|
'default' => false,
|
||||||
|
'type' => 'switcher',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'order_expired_time',
|
||||||
|
'title' => __('订单时效有效期数值', 'aksrt'),
|
||||||
|
'default' => 0,
|
||||||
|
'type' => 'number',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => __('订单时效有效期单位', 'aksrt'),
|
||||||
|
'type' => 'select',
|
||||||
|
'class' => 'compact',
|
||||||
|
'id' => 'order_expired_unit',
|
||||||
|
'options' => [
|
||||||
|
'day' => __('天', 'aksrt'),
|
||||||
|
'hour' => __('小时', 'aksrt'),
|
||||||
|
'minute' => __('分钟', 'aksrt'),
|
||||||
|
],
|
||||||
|
'default' => 'day',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,776 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin post list enhancements: interaction + sales columns and the
|
||||||
|
* quick/bulk edit fields behind them.
|
||||||
|
*
|
||||||
|
* Mirrors the reference theme's post list: a sortable “销售数据” column, a
|
||||||
|
* product-type filter, and four quick-edit field groups (interaction data,
|
||||||
|
* article config, pay parameters, custom filters). The reference theme hooks
|
||||||
|
* those boxes onto its topics taxonomy column; this project has no topics
|
||||||
|
* taxonomy, so they hang off the interaction-data column instead.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin\Native;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class PostListEnhance {
|
||||||
|
|
||||||
|
public const COLUMN_STATS = 'aksrt_stats';
|
||||||
|
public const COLUMN_SALES = 'aksrt_sales';
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/** Real sales counts for the current page, keyed by post ID. @var array<int,int>|null */
|
||||||
|
private $sales_map = null;
|
||||||
|
|
||||||
|
private function __construct() {}
|
||||||
|
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void {
|
||||||
|
add_filter('manage_post_posts_columns', [$this, 'register_columns']);
|
||||||
|
add_action('manage_post_posts_custom_column', [$this, 'render_column'], 10, 2);
|
||||||
|
add_action('restrict_manage_posts', [$this, 'render_type_filter']);
|
||||||
|
add_action('pre_get_posts', [$this, 'apply_query_args'], 99);
|
||||||
|
|
||||||
|
add_action('quick_edit_custom_box', [$this, 'render_edit_boxes'], 50, 2);
|
||||||
|
add_action('bulk_edit_custom_box', [$this, 'render_edit_boxes'], 50, 2);
|
||||||
|
add_action('edit_post', [$this, 'save_edit_boxes'], 10, 2);
|
||||||
|
|
||||||
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Assets
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public function enqueue_assets(string $hook): void {
|
||||||
|
if ($hook !== 'edit.php') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||||
|
if (!$screen || $screen->post_type !== 'post' || $screen->base !== 'edit') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('aksrt_enqueue_fontawesome')) {
|
||||||
|
aksrt_enqueue_fontawesome();
|
||||||
|
}
|
||||||
|
|
||||||
|
$css = AKSRT_DIR . '/assets/css/admin-main.css';
|
||||||
|
if (is_file($css)) {
|
||||||
|
wp_enqueue_style('aksrt-admin-main', AKSRT_URI . '/assets/css/admin-main.css', ['aksrt-font-awesome'], (string) filemtime($css));
|
||||||
|
}
|
||||||
|
$js = AKSRT_DIR . '/assets/js/admin-post-list.js';
|
||||||
|
if (is_file($js)) {
|
||||||
|
wp_enqueue_script('aksrt-post-list', AKSRT_URI . '/assets/js/admin-post-list.js', [], (string) filemtime($js), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Columns
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert the interaction and sales columns after the title column.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $columns
|
||||||
|
* @return array<string,string>
|
||||||
|
*/
|
||||||
|
public function register_columns($columns) {
|
||||||
|
if (!is_array($columns)) {
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = $this->current_order();
|
||||||
|
$order_by = $this->current_orderby();
|
||||||
|
$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) {
|
||||||
|
$new_columns[$key] = $label;
|
||||||
|
if ($key !== 'title') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$new_columns[self::COLUMN_STATS] = __('互动数据', 'aksrt');
|
||||||
|
$new_columns[self::COLUMN_SALES] = '<a href="' . esc_url(add_query_arg(['orderby' => 'sales_volume', 'order' => $order])) . '"><span>' . esc_html__('销售数据', 'aksrt') . ($order_by === 'sales_volume' ? $o_icon : '') . '</span></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $column
|
||||||
|
* @param int $post_id
|
||||||
|
*/
|
||||||
|
public function render_column($column, $post_id): void {
|
||||||
|
if ((string) $column === self::COLUMN_STATS) {
|
||||||
|
$this->render_stats_column((int) $post_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((string) $column === self::COLUMN_SALES) {
|
||||||
|
$this->render_sales_column((int) $post_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_stats_column(int $post_id): void {
|
||||||
|
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true);
|
||||||
|
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : (int) get_post_meta($post_id, '_aksrt_like_count', true);
|
||||||
|
$favorites = function_exists('aksrt_get_post_favorites') ? (int) aksrt_get_post_favorites($post_id) : (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
|
||||||
|
|
||||||
|
$format = static function (int $n): string {
|
||||||
|
if (function_exists('aksrt_format_number')) {
|
||||||
|
return (string) aksrt_format_number($n);
|
||||||
|
}
|
||||||
|
if ($n >= 10000) {
|
||||||
|
return round($n / 10000, 1) . 'W+';
|
||||||
|
}
|
||||||
|
if ($n >= 1000) {
|
||||||
|
return round($n / 1000, 1) . 'K';
|
||||||
|
}
|
||||||
|
return (string) $n;
|
||||||
|
};
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
|
||||||
|
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
|
||||||
|
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
|
||||||
|
|
||||||
|
echo implode(' · ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_sales_column(int $post_id): void {
|
||||||
|
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : [];
|
||||||
|
$pay_type = (string) ($pay_meta['pay_type'] ?? '');
|
||||||
|
if ($pay_type === '' || $pay_type === \AKSRT\Core\DomainModel::PAY_TYPE_NO) {
|
||||||
|
echo '<div style="font-size: 12px;">' . esc_html__('未开启付费', 'aksrt') . '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pay_modo = (string) ($pay_meta['pay_modo'] ?? '0');
|
||||||
|
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||||
|
$type_label = $type_labels[$pay_type] ?? $pay_type;
|
||||||
|
$type_color = $this->pay_type_color($pay_type);
|
||||||
|
|
||||||
|
if ($pay_modo === 'points') {
|
||||||
|
$price_text = sprintf(__('积分:%s', 'aksrt'), (string) max(0, (int) ($pay_meta['points_price'] ?? 0)));
|
||||||
|
} else {
|
||||||
|
$price_text = $this->currency_symbol() . ':' . (string) ($pay_meta['price'] ?? '0.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表列与前台同一口径:真实订单 + 后台「销量浮动」(pay_count)。
|
||||||
|
// 真实销量取订单表统计,_aksrt_sales_volume 只是订单流水累加的副本。
|
||||||
|
$real_sales = $this->real_sales_count($post_id);
|
||||||
|
$meta_sales = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
||||||
|
$sales_volume = max($real_sales, $meta_sales) + max(0, (int) ($pay_meta['pay_count'] ?? 0));
|
||||||
|
$real_html = $real_sales > 0
|
||||||
|
? ' <a title="' . esc_attr__('点击查看销售明细', 'aksrt') . '" href="' . esc_url($this->orders_url($post_id)) . '">[' . esc_html(sprintf(__('真实销量%s', 'aksrt'), $real_sales)) . ']</a>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$link = add_query_arg(['aksrt_pay_type' => $pay_type]);
|
||||||
|
echo '<div>';
|
||||||
|
echo '<a style="color: ' . esc_attr($type_color) . ';" href="' . esc_url($link) . '">' . esc_html($type_label) . '</a>';
|
||||||
|
echo '<div style="font-size: 12px;">' . esc_html($price_text) . ' · ' . esc_html__('销量', 'aksrt') . esc_html((string) $sales_volume) . wp_kses_post($real_html) . '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color palette for the pay-type link, mirroring the reference theme.
|
||||||
|
*/
|
||||||
|
private function pay_type_color(string $pay_type): string {
|
||||||
|
$palette = [
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_READING => '#ff4747',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD => '#ee5307',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_GALLERY => '#1e8608',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_VIDEO => '#086ae8',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_VIP => '#3353fd',
|
||||||
|
];
|
||||||
|
return $palette[$pay_type] ?? '#464646';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Filter + query
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public function render_type_filter(string $post_type): void {
|
||||||
|
if ($post_type !== 'post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
|
||||||
|
$labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||||
|
unset($labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
||||||
|
|
||||||
|
echo '<select name="aksrt_pay_type">';
|
||||||
|
echo '<option value="">' . esc_html__('商品类型', 'aksrt') . '</option>';
|
||||||
|
foreach ($labels as $key => $label) {
|
||||||
|
printf('<option %s value="%s">%s</option>', selected($current, $key, false), esc_attr($key), esc_html($label));
|
||||||
|
}
|
||||||
|
echo '</select>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the header orderby links and the product-type filter onto the query.
|
||||||
|
*/
|
||||||
|
public function apply_query_args($query): void {
|
||||||
|
if (!$query instanceof \WP_Query || !is_admin() || !$query->is_main_query()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isset($_GET['post_type']) || sanitize_key((string) wp_unslash($_GET['post_type'])) !== 'post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : '';
|
||||||
|
if ($orderby === 'sales_volume') {
|
||||||
|
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY);
|
||||||
|
$query->set('orderby', 'meta_value_num');
|
||||||
|
} elseif ($orderby === 'views' || $orderby === 'like' || $orderby === 'favorite') {
|
||||||
|
$query->set('meta_key', $this->stats_meta_key($orderby));
|
||||||
|
$query->set('orderby', 'meta_value_num');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pay_type = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
|
||||||
|
if ($pay_type !== '' && isset(\AKSRT\Core\DomainModel::pay_type_labels()[$pay_type])) {
|
||||||
|
$meta_query = $query->get('meta_query');
|
||||||
|
$meta_query = is_array($meta_query) ? $meta_query : [];
|
||||||
|
$meta_query[] = [
|
||||||
|
'relation' => 'OR',
|
||||||
|
[
|
||||||
|
'key' => \AKSRT\Core\DomainModel::PAY_TYPE_KEY,
|
||||||
|
'value' => $pay_type,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => \AKSRT\Core\DomainModel::PAY_META_KEY,
|
||||||
|
'value' => '"pay_type";s:' . strlen($pay_type) . ':"' . $pay_type . '"',
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$query->set('meta_query', $meta_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public orderby key → stored meta key for the interaction column.
|
||||||
|
*/
|
||||||
|
private function stats_meta_key(string $orderby): string {
|
||||||
|
$map = [
|
||||||
|
'views' => \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY,
|
||||||
|
'like' => '_aksrt_like_count',
|
||||||
|
'favorite' => '_aksrt_favorite_count',
|
||||||
|
];
|
||||||
|
return $map[$orderby] ?? \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Quick / bulk edit boxes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $column_name
|
||||||
|
* @param string $post_type
|
||||||
|
*/
|
||||||
|
public function render_edit_boxes($column_name, $post_type): void {
|
||||||
|
if ($post_type !== 'post' || (string) $column_name !== self::COLUMN_STATS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单控件(select/input/fieldset)不在 wp_kses_post 的文章白名单里,
|
||||||
|
// 会被剥成纯文本,因此这里直接输出;字段值均已 esc_attr/esc_html 转义。
|
||||||
|
echo $this->edit_boxes_html();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function edit_boxes_html(): string {
|
||||||
|
$html = $this->fields_html(PostListEditFields::extended(), 'extended');
|
||||||
|
$html .= $this->fields_html(PostListEditFields::pay($this->currency_symbol()), 'pay', __('付费参数', 'aksrt'), '<div class="c-yellow">' . esc_html__('批量设置付费参数,请仔细检查配置内容,更新后无法撤回。请注意配置逻辑,例如会员价不能高于普通价,执行价不能高于原价等', 'aksrt') . '</div>');
|
||||||
|
$html .= $this->filter_fields_html();
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom-filter dimensions come from settings, so they are rendered
|
||||||
|
* separately from the static field lists.
|
||||||
|
*/
|
||||||
|
private function filter_fields_html(): string {
|
||||||
|
$dimensions = [];
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
|
||||||
|
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
|
||||||
|
}
|
||||||
|
if (!$dimensions) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = [];
|
||||||
|
foreach ($dimensions as $dimension) {
|
||||||
|
$key = sanitize_key((string) ($dimension['key'] ?? ''));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$options = [];
|
||||||
|
foreach ((array) ($dimension['vals'] ?? []) as $value) {
|
||||||
|
$value_key = sanitize_key((string) ($value['key'] ?? ''));
|
||||||
|
if ($value_key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$options[$value_key] = (string) ($value['name'] ?? $value_key);
|
||||||
|
}
|
||||||
|
if (!$options) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fields[] = [
|
||||||
|
'id' => $key,
|
||||||
|
'title' => (string) ($dimension['name'] ?? $key),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'options' => $options,
|
||||||
|
'placeholder' => sprintf(__('请选择%s', 'aksrt'), (string) ($dimension['name'] ?? $key)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$fields) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->fields_html($fields, 'filter', __('高级筛选', 'aksrt'), '<div class="px12 aksrt-bulk-edit-desc">' . esc_html__('让文章实现更加精细化的分类,同时方便用户进行文章筛选', 'aksrt') . '</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render one field group: the shared builder for every quick-edit box.
|
||||||
|
*
|
||||||
|
* $desc 是调用方预先转义好的 HTML 片段,这里原样输出(付费参数那组
|
||||||
|
* 自带 <div class="c-yellow">,转义会把标签显示成文字)。
|
||||||
|
*
|
||||||
|
* @param array<int,array<string,mixed>> $fields
|
||||||
|
*/
|
||||||
|
private function fields_html(array $fields, string $id, string $title = '', string $desc = ''): string {
|
||||||
|
$item_html = '';
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$options_html = ' ';
|
||||||
|
switch ($field['type']) {
|
||||||
|
case 'number':
|
||||||
|
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
|
||||||
|
$options_html = '<div class="flex ac hh">'
|
||||||
|
. '<select name="' . esc_attr($name) . '[operation]">'
|
||||||
|
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'aksrt') . '</option>'
|
||||||
|
. '<option value="set">' . esc_html__('设置为', 'aksrt') . '</option>'
|
||||||
|
. '<option value="plus">' . esc_html__('加', 'aksrt') . '</option>'
|
||||||
|
. '<option value="subtract">' . esc_html__('减', 'aksrt') . '</option>'
|
||||||
|
. '<option value="multiply">' . esc_html__('乘', 'aksrt') . '</option>'
|
||||||
|
. '<option value="division">' . esc_html__('除', 'aksrt') . '</option>'
|
||||||
|
. '</select>'
|
||||||
|
. '<input type="text" name="' . esc_attr($name) . '[val]" value="">'
|
||||||
|
. '</div>';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
$select_options = !empty($field['options']) ? $field['options'] : [];
|
||||||
|
if ($select_options) {
|
||||||
|
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
|
||||||
|
$options_html = $this->checkbox_options_html($name, $select_options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$field['options'] = [
|
||||||
|
'ignore' => __('不更改', 'aksrt'),
|
||||||
|
'1' => __('开启', 'aksrt'),
|
||||||
|
'0' => __('关闭', 'aksrt'),
|
||||||
|
];
|
||||||
|
// Falls through to the switcher renderer below.
|
||||||
|
|
||||||
|
case 'switcher':
|
||||||
|
$field['options'] = [
|
||||||
|
'ignore' => __('不更改', 'aksrt'),
|
||||||
|
'1' => __('开启', 'aksrt'),
|
||||||
|
'0' => __('关闭', 'aksrt'),
|
||||||
|
];
|
||||||
|
// Falls through to the select renderer below.
|
||||||
|
|
||||||
|
case 'radio':
|
||||||
|
case 'select':
|
||||||
|
$options = isset($field['options']) && is_array($field['options']) ? $field['options'] : [];
|
||||||
|
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
|
||||||
|
$options_html = '<div class="flex ac hh">'
|
||||||
|
. '<select name="' . esc_attr($name) . '">'
|
||||||
|
. '<option selected="selected" value="ignore">--' . esc_html__('不更改', 'aksrt') . '--</option>';
|
||||||
|
foreach ($options as $key => $value) {
|
||||||
|
$options_html .= '<option value="' . esc_attr((string) $key) . '">' . esc_html((string) $value) . '</option>';
|
||||||
|
}
|
||||||
|
$options_html .= '</select></div>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($field['id']) || $options_html === ' ') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title_html = !empty($field['title']) ? (string) $field['title'] : (!empty($field['label']) ? (string) $field['label'] : '');
|
||||||
|
$desc_html = !empty($field['desc']) ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html((string) $field['desc']) . '</div>' : '';
|
||||||
|
$subtitle = !empty($field['subtitle']) ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html((string) $field['subtitle']) . '</div>' : '';
|
||||||
|
$dependency = !empty($field['dependency']) ? ' data-dependency="' . esc_attr(wp_json_encode($field['dependency'])) . '"' : '';
|
||||||
|
|
||||||
|
$item_html .= '<div class="aksrt-bulk-edit-box"' . $dependency . '>'
|
||||||
|
. '<div>' . esc_html($title_html) . '</div>'
|
||||||
|
. $options_html . $subtitle . $desc_html
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($item_html === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// $desc 由调用方传入并保证安全(均为代码内常量),故原样输出:
|
||||||
|
// 付费参数那组自带 <div class="c-yellow">,转义会让标签直接显示成文字。
|
||||||
|
$legend = $title !== '' ? '<legend class="inline-edit-legend">' . esc_html($title) . '</legend>' : '';
|
||||||
|
|
||||||
|
return '<fieldset style="margin-top: 20px">' . $legend . '<div class="flex hh">' . $item_html . '</div>' . $desc . '</fieldset>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,string> $options
|
||||||
|
*/
|
||||||
|
private function checkbox_options_html(string $name, array $options): string {
|
||||||
|
$html = '<select class="mr10" name="' . esc_attr($name) . '[operation]">'
|
||||||
|
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'aksrt') . '</option>'
|
||||||
|
. '<option value="set">' . esc_html__('设置为', 'aksrt') . '</option>'
|
||||||
|
. '</select>';
|
||||||
|
|
||||||
|
foreach ($options as $key => $value) {
|
||||||
|
$html .= '<div class="mr10"><label><input value="' . esc_attr((string) $key) . '" type="checkbox" name="' . esc_attr($name) . '[val][]"> ' . esc_html((string) $value) . '</label></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="flex ac hh">' . $html . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Save
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist quick/bulk edit submissions for the current post.
|
||||||
|
*
|
||||||
|
* @param int $post_id
|
||||||
|
* @param \WP_Post $post
|
||||||
|
*/
|
||||||
|
public function save_edit_boxes(int $post_id, $post): void {
|
||||||
|
if (!$post instanceof \WP_Post || $post->post_type !== 'post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wp_is_post_revision($post_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!current_user_can('edit_post', $post_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$screen = isset($_POST['screen']) ? sanitize_key((string) wp_unslash($_POST['screen'])) : '';
|
||||||
|
if ($screen !== 'edit-post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$submitted = isset($_POST['aksrt_bulk_edit']) && is_array($_POST['aksrt_bulk_edit']) ? wp_unslash($_POST['aksrt_bulk_edit']) : [];
|
||||||
|
if (!$submitted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($submitted['extended'])) {
|
||||||
|
$this->save_extended($post_id, (array) $submitted['extended']);
|
||||||
|
}
|
||||||
|
if (!empty($submitted['pay'])) {
|
||||||
|
$this->save_pay($post_id, (array) $submitted['pay']);
|
||||||
|
}
|
||||||
|
if (!empty($submitted['filter'])) {
|
||||||
|
$this->save_filters($post_id, (array) $submitted['filter']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $fields
|
||||||
|
*/
|
||||||
|
private function save_extended(int $post_id, array $fields): void {
|
||||||
|
foreach ($fields as $field_id => $field_value) {
|
||||||
|
$field_id = sanitize_key((string) $field_id);
|
||||||
|
switch ($field_id) {
|
||||||
|
case 'views':
|
||||||
|
case 'like':
|
||||||
|
if (!is_array($field_value) || empty($field_value['operation']) || empty($field_value['val']) || !is_numeric($field_value['val'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$operation = sanitize_key((string) $field_value['operation']);
|
||||||
|
if ($operation === 'ignore') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$value = (float) $field_value['val'];
|
||||||
|
$old = (float) get_post_meta($post_id, $this->stats_meta_key($field_id), true);
|
||||||
|
$new = $this->apply_operation($old, $value, $operation);
|
||||||
|
if ($new !== null) {
|
||||||
|
update_post_meta($post_id, $this->stats_meta_key($field_id), max(0, (int) $new));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'show_layout':
|
||||||
|
case 'no_article-navs':
|
||||||
|
case 'article_maxheight_xz':
|
||||||
|
$value = is_scalar($field_value) ? (string) $field_value : 'ignore';
|
||||||
|
if ($value === 'ignore' || $value === '') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($field_id === 'show_layout') {
|
||||||
|
$value = in_array($value, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true) ? $value : 'false';
|
||||||
|
} else {
|
||||||
|
$value = in_array($value, ['1', '0'], true) ? $value : '0';
|
||||||
|
}
|
||||||
|
$this->update_post_meta_bag($post_id, $field_id, $value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $fields
|
||||||
|
*/
|
||||||
|
private function save_pay(int $post_id, array $fields): void {
|
||||||
|
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : [];
|
||||||
|
if (!is_array($pay_meta) || $pay_meta === []) {
|
||||||
|
$pay_meta = \AKSRT\Core\DomainModel::default_pay_meta();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $field_id => $field_value) {
|
||||||
|
$field_id = sanitize_key((string) $field_id);
|
||||||
|
switch ($field_id) {
|
||||||
|
case 'pay_type':
|
||||||
|
case 'pay_limit':
|
||||||
|
case 'pay_modo':
|
||||||
|
if (is_scalar($field_value) && (string) $field_value !== 'ignore') {
|
||||||
|
$pay_meta[$field_id] = sanitize_key((string) $field_value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'coupon_s':
|
||||||
|
case 'order_expired_s':
|
||||||
|
case 'show_sales':
|
||||||
|
if (is_scalar($field_value) && in_array((string) $field_value, ['1', '0'], true)) {
|
||||||
|
$pay_meta[$field_id] = (string) $field_value === '1';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'order_expired_unit':
|
||||||
|
if (is_scalar($field_value) && in_array((string) $field_value, ['day', 'hour', 'minute'], true)) {
|
||||||
|
$pay_meta[$field_id] = (string) $field_value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'points_price':
|
||||||
|
case 'vip1_points':
|
||||||
|
case 'vip2_points':
|
||||||
|
case 'order_expired_time':
|
||||||
|
case 'pay_count':
|
||||||
|
if (!is_array($field_value) || empty($field_value['operation']) || empty($field_value['val']) || !is_numeric($field_value['val'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$operation = sanitize_key((string) $field_value['operation']);
|
||||||
|
if ($operation === 'ignore') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$current = (float) ($pay_meta[$field_id] ?? 0);
|
||||||
|
$new = $this->apply_operation($current, (float) $field_value['val'], $operation);
|
||||||
|
if ($new !== null) {
|
||||||
|
$pay_meta[$field_id] = max(0, (int) $new);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'price':
|
||||||
|
case 'original_price':
|
||||||
|
case 'vip1_price':
|
||||||
|
case 'vip2_price':
|
||||||
|
if (!is_array($field_value) || empty($field_value['operation']) || empty($field_value['val']) || !is_numeric($field_value['val'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$operation = sanitize_key((string) $field_value['operation']);
|
||||||
|
if ($operation === 'ignore') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$current = (string) ($pay_meta[$field_id] ?? '0.00');
|
||||||
|
try {
|
||||||
|
$current_minor = (float) \AKSRT\Ledger\AssetAmount::balance_minor($current);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$current_minor = 0.0;
|
||||||
|
}
|
||||||
|
$new = $this->apply_operation($current_minor, (float) $field_value['val'], $operation);
|
||||||
|
if ($new !== null) {
|
||||||
|
$pay_meta[$field_id] = \AKSRT\Ledger\AssetAmount::decimal_from_minor(max(0, (int) round($new)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pay_meta = \AKSRT\Core\DomainModel::sanitize_pay_meta($pay_meta);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::PAY_META_KEY, $pay_meta);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::PAY_TYPE_KEY, (string) $pay_meta['pay_type']);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::PAY_PRICE_KEY, $pay_meta['pay_modo'] === 'points' ? (int) $pay_meta['points_price'] : (string) $pay_meta['price']);
|
||||||
|
update_post_meta($post_id, '_aksrt_pay_modo', (string) $pay_meta['pay_modo']);
|
||||||
|
update_post_meta($post_id, '_aksrt_pay_limit', (int) $pay_meta['pay_limit']);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_MARKER_KEY, \AKSRT\Core\DomainModel::is_resource_pay_type((string) $pay_meta['pay_type']) ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $fields
|
||||||
|
*/
|
||||||
|
private function save_filters(int $post_id, array $fields): void {
|
||||||
|
$dimensions = [];
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
|
||||||
|
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
|
||||||
|
}
|
||||||
|
if (!$dimensions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_dimensions = [];
|
||||||
|
foreach ($dimensions as $dimension) {
|
||||||
|
$key = sanitize_key((string) ($dimension['key'] ?? ''));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$allowed_dimensions[$key] = array_values(array_filter(array_map(static function ($value): string {
|
||||||
|
return sanitize_key((string) ($value['key'] ?? ''));
|
||||||
|
}, (array) ($dimension['vals'] ?? []))));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $field_id => $field_value) {
|
||||||
|
$field_id = sanitize_key((string) $field_id);
|
||||||
|
if (!isset($allowed_dimensions[$field_id]) || !is_array($field_value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($field_value['operation']) || sanitize_key((string) $field_value['operation']) !== 'set') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$values = isset($field_value['val']) && is_array($field_value['val'])
|
||||||
|
? array_map('sanitize_key', $field_value['val'])
|
||||||
|
: [];
|
||||||
|
$values = array_values(array_intersect($allowed_dimensions[$field_id], $values));
|
||||||
|
if ($values) {
|
||||||
|
update_post_meta($post_id, '_aksrt_cf_' . $field_id, $values);
|
||||||
|
} else {
|
||||||
|
delete_post_meta($post_id, '_aksrt_cf_' . $field_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge one field into the post-meta bag shared with the article extension
|
||||||
|
* meta box.
|
||||||
|
*/
|
||||||
|
private function update_post_meta_bag(int $post_id, string $key, string $value): void {
|
||||||
|
$bag_key = \AKSRT\Core\PostMetaBox::META_KEY;
|
||||||
|
$bag = get_post_meta($post_id, $bag_key, true);
|
||||||
|
$bag = is_array($bag) ? $bag : [];
|
||||||
|
$bag[$key] = $value;
|
||||||
|
update_post_meta($post_id, $bag_key, $bag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an operation, returning null when it must be ignored.
|
||||||
|
*/
|
||||||
|
private function apply_operation(float $current, float $value, string $operation): ?float {
|
||||||
|
switch ($operation) {
|
||||||
|
case 'set':
|
||||||
|
return $value;
|
||||||
|
case 'plus':
|
||||||
|
return round($current + $value, 2);
|
||||||
|
case 'subtract':
|
||||||
|
return round($current - $value, 2);
|
||||||
|
case 'multiply':
|
||||||
|
return round($current * $value, 2);
|
||||||
|
case 'division':
|
||||||
|
return ($current != 0 && $value != 0) ? round($current / $value, 2) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configured cash symbol, shared by the sales column and the pay fields.
|
||||||
|
*/
|
||||||
|
private function currency_symbol(): string {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
|
||||||
|
return (string) ($paid['display']['currency_symbol'] ?? '¥');
|
||||||
|
}
|
||||||
|
return '¥';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function current_order(): string {
|
||||||
|
$order = isset($_REQUEST['order']) ? sanitize_key((string) wp_unslash($_REQUEST['order'])) : 'desc';
|
||||||
|
return $order === 'asc' ? 'asc' : 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function current_orderby(): string {
|
||||||
|
return isset($_REQUEST['orderby']) ? sanitize_key((string) wp_unslash($_REQUEST['orderby'])) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real paid orders for one post, batched across the current page.
|
||||||
|
*/
|
||||||
|
private function real_sales_count(int $post_id): int {
|
||||||
|
if ($this->sales_map === null) {
|
||||||
|
$this->sales_map = $this->load_sales_map();
|
||||||
|
}
|
||||||
|
return (int) ($this->sales_map[$post_id] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,int>
|
||||||
|
*/
|
||||||
|
private function load_sales_map(): array {
|
||||||
|
global $wp_query;
|
||||||
|
if (!$wp_query instanceof \WP_Query || empty($wp_query->posts)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = [];
|
||||||
|
foreach ($wp_query->posts as $post) {
|
||||||
|
if ($post instanceof \WP_Post) {
|
||||||
|
$ids[] = (int) $post->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$ids) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_map(
|
||||||
|
$ids,
|
||||||
|
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
|
||||||
|
);
|
||||||
|
$out = [];
|
||||||
|
foreach ($map as $pid => $row) {
|
||||||
|
$out[(int) $pid] = (int) ($row['sales_count'] ?? 0);
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function orders_url(int $post_id): string {
|
||||||
|
return add_query_arg(
|
||||||
|
[
|
||||||
|
'page' => 'aksrt_orders',
|
||||||
|
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||||
|
'product_id' => $post_id,
|
||||||
|
],
|
||||||
|
admin_url('admin.php')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ class GlobalSchema {
|
|||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
||||||
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
||||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需先在 WordPress 配置 AI Connectors。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
||||||
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
||||||
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
||||||
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
||||||
@@ -79,12 +79,6 @@ class GlobalSchema {
|
|||||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
|
|
||||||
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
|
|
||||||
]],
|
|
||||||
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
||||||
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ class SchemaExtensionsPush {
|
|||||||
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||||
]],
|
]],
|
||||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
['key' => 'channel', 'title' => '收录通道', 'fields' => [
|
||||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
|
||||||
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
|
|
||||||
]],
|
]],
|
||||||
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
||||||
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
|
||||||
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
|
|||||||
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
||||||
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
||||||
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
||||||
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class SchemaPayment {
|
|||||||
['key' => 'pay_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
['key' => 'pay_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
||||||
['key' => 'payment_show_ip', 'label' => '显示用户IP', 'type' => 'switch', 'default' => false],
|
['key' => 'payment_show_ip', 'label' => '显示用户IP', 'type' => 'switch', 'default' => false],
|
||||||
['key' => 'payment_show_device', 'label' => '显示设备信息', 'type' => 'switch', 'default' => false],
|
['key' => 'payment_show_device', 'label' => '显示设备信息', 'type' => 'switch', 'default' => false],
|
||||||
['key' => 'payment_show_order_no', 'label' => '显示流水号', 'type' => 'switch', 'default' => true],
|
['key' => 'payment_show_order_no', 'label' => '显示订单号', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'payment_show_create_time', 'label' => '显示创建时间', 'type' => 'switch', 'default' => true],
|
['key' => 'payment_show_create_time', 'label' => '显示创建时间', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'payment_show_pay_time', 'label' => '显示支付时间', 'type' => 'switch', 'default' => true],
|
['key' => 'payment_show_pay_time', 'label' => '显示支付时间', 'type' => 'switch', 'default' => true],
|
||||||
]],
|
]],
|
||||||
@@ -81,8 +81,6 @@ class SchemaPayment {
|
|||||||
['key' => 'pay_type_option', 'label' => '商品功能配置', 'type' => 'tabbed', 'tabs' => [
|
['key' => 'pay_type_option', 'label' => '商品功能配置', 'type' => 'tabbed', 'tabs' => [
|
||||||
['key' => 'download', 'title' => '付费下载', 'fields' => [
|
['key' => 'download', 'title' => '付费下载', 'fields' => [
|
||||||
['key' => 'down_alone_page', 'label' => '独立下载页面', 'type' => 'switch', 'default' => false, 'help' => '开启后,付费资源的下载链接会跳转独立页面显示'],
|
['key' => 'down_alone_page', 'label' => '独立下载页面', 'type' => 'switch', 'default' => false, 'help' => '开启后,付费资源的下载链接会跳转独立页面显示'],
|
||||||
['key' => 'down_user_log', 'label' => '保存用户下载记录(条)', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '填0则不保存,注意记录量太大可能会影响网站性能'],
|
|
||||||
['key' => 'down_post_log', 'label' => '保存文章下载记录(条)', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '填0则不保存,注意记录量太大可能会影响网站性能'],
|
|
||||||
['key' => 'down_remote_to_local_s', 'label' => '远程文件转本地文件下载', 'type' => 'switch', 'default' => true, 'help' => '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址'],
|
['key' => 'down_remote_to_local_s', 'label' => '远程文件转本地文件下载', 'type' => 'switch', 'default' => true, 'help' => '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址'],
|
||||||
['key' => 'down_remote_to_local_size', 'label' => '文件大小限制(Kb)', 'type' => 'number', 'default' => 2048, 'min' => 0],
|
['key' => 'down_remote_to_local_size', 'label' => '文件大小限制(Kb)', 'type' => 'number', 'default' => 2048, 'min' => 0],
|
||||||
['key' => 'down_remote_to_local_ext', 'label' => '文件格式限制', 'type' => 'input', 'default' => 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', 'help' => '限制使用此功能的文件格式,多个用英文逗号隔开'],
|
['key' => 'down_remote_to_local_ext', 'label' => '文件格式限制', 'type' => 'input', 'default' => 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', 'help' => '限制使用此功能的文件格式,多个用英文逗号隔开'],
|
||||||
@@ -161,7 +159,7 @@ class SchemaPayment {
|
|||||||
['key' => 'points_pass_exchange_desc', 'label' => '卡密兑换积分说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
['key' => 'points_pass_exchange_desc', 'label' => '卡密兑换积分说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||||
['key' => 'points_pass_exchange_only_password', 'label' => '积分卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '积分卡密仅需输入密码,不需要卡号'],
|
['key' => 'points_pass_exchange_only_password', 'label' => '积分卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '积分卡密仅需输入密码,不需要卡号'],
|
||||||
['key' => 'pay_vip_pass_charge_s', 'label' => '启用卡密兑换会员功能', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员'],
|
['key' => 'pay_vip_pass_charge_s', 'label' => '启用卡密兑换会员功能', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员'],
|
||||||
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||||
['key' => 'pay_vip_pass_charge_only_password', 'label' => '会员卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)'],
|
['key' => 'pay_vip_pass_charge_only_password', 'label' => '会员卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)'],
|
||||||
]],
|
]],
|
||||||
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
||||||
@@ -180,7 +178,7 @@ class SchemaPayment {
|
|||||||
'vip' => [
|
'vip' => [
|
||||||
'key' => 'vip',
|
'key' => 'vip',
|
||||||
'title' => 'VIP会员',
|
'title' => 'VIP会员',
|
||||||
'version' => '1.1.0',
|
'version' => '1.2.0',
|
||||||
'option_key' => 'aksrt_settings',
|
'option_key' => 'aksrt_settings',
|
||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||||
@@ -190,17 +188,8 @@ class SchemaPayment {
|
|||||||
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
||||||
['key' => 'nav_user_pay_vip', 'label' => '用户框购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在导航栏用户框内显示开通会员按钮。'],
|
['key' => 'nav_user_pay_vip', 'label' => '用户框购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在导航栏用户框内显示开通会员按钮。'],
|
||||||
['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true],
|
['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'pay_user_vip_1_name', 'label' => '一级会员显示名称', 'type' => 'input', 'default' => '一级会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
['key' => 'vip_level_order_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'danger', 'dependsOn' => ['all' => [['key' => 'pay_user_vip_2_s', 'value' => true], ['key' => 'pay_user_vip_1_s', 'operator' => 'falsy']]], 'content' => '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>'],
|
||||||
['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享一级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
|
||||||
['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
|
||||||
['key' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '留空使用内置 vip-1.svg 图标'],
|
|
||||||
['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'pay_user_vip_2_name', 'label' => '二级会员显示名称', 'type' => 'input', 'default' => '二级会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
|
||||||
['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享二级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
|
||||||
['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
|
||||||
['key' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '留空使用内置 vip-2.svg 图标'],
|
|
||||||
['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''],
|
|
||||||
['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''],
|
|
||||||
]],
|
]],
|
||||||
['key' => 'benefit', 'title' => '会员权益', 'fields' => [
|
['key' => 'benefit', 'title' => '会员权益', 'fields' => [
|
||||||
['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [
|
['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [
|
||||||
@@ -208,11 +197,11 @@ class SchemaPayment {
|
|||||||
['key' => 'pay_download_limit', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '普通用户每日最多可下载几个免费资源(为0则不限制)'],
|
['key' => 'pay_download_limit', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '普通用户每日最多可下载几个免费资源(为0则不限制)'],
|
||||||
['key' => 'pay_download_speed', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
['key' => 'pay_download_speed', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||||
]],
|
]],
|
||||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => [
|
||||||
['key' => 'pay_download_limit_vip_1', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '一级会员每日最多可下载几个免费资源(为0则不限制)'],
|
['key' => 'pay_download_limit_vip_1', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '一级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||||
['key' => 'pay_download_speed_vip_1', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
['key' => 'pay_download_speed_vip_1', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||||
]],
|
]],
|
||||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => [
|
||||||
['key' => 'pay_download_limit_vip_2', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '二级会员每日最多可下载几个免费资源(为0则不限制)'],
|
['key' => 'pay_download_limit_vip_2', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '二级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||||
['key' => 'pay_download_speed_vip_2', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
['key' => 'pay_download_speed_vip_2', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||||
]],
|
]],
|
||||||
@@ -220,36 +209,110 @@ class SchemaPayment {
|
|||||||
]],
|
]],
|
||||||
['key' => 'opt', 'title' => '会员购买选项', 'fields' => [
|
['key' => 'opt', 'title' => '会员购买选项', 'fields' => [
|
||||||
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
||||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => self::vip_level_tab_fields(1)],
|
||||||
['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => self::vip_level_tab_fields(2)],
|
||||||
['key' => 'payment_vip_1_products', 'label' => '一级会员套餐', 'type' => 'textarea', 'default' => "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_1_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
|
||||||
]],
|
|
||||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
|
||||||
['key' => 'vip_2_product_s', 'label' => '允许购买二级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
|
||||||
['key' => 'payment_vip_2_products', 'label' => '二级会员套餐', 'type' => 'textarea', 'default' => "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_2_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
|
||||||
]],
|
|
||||||
['key' => 'renew', 'title' => '会员续费', 'fields' => [
|
['key' => 'renew', 'title' => '会员续费', 'fields' => [
|
||||||
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'],
|
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'],
|
||||||
['key' => 'vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_renew', 'value' => true]],
|
['key' => 'vip_renew_desc', 'label' => '续费介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '立即续费会员,畅享VIP权益', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'help' => '续费会员的一句话简介,支持HTML'],
|
||||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'select', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||||
['label' => '相同价格', 'value' => 'same'],
|
['label' => '相同价格', 'value' => 'same'],
|
||||||
['label' => '折扣价格', 'value' => 'discount'],
|
['label' => '折扣价格', 'value' => 'discount'],
|
||||||
['label' => '优惠立减', 'value' => 'reduce'],
|
['label' => '优惠立减', 'value' => 'reduce'],
|
||||||
]],
|
['label' => '自定义商品', 'value' => 'customize'],
|
||||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
], 'help' => '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价'],
|
||||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
['key' => 'vip_renew_discount', 'label' => '续费折扣', 'type' => 'slider', 'sanitize_type' => 'float', 'default' => 8, 'min' => 0.1, 'max' => 9.9, 'step' => 0.1, 'unit' => '折', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount'], 'help' => '在会员商品基础上打几折,如 8 = 八折'],
|
||||||
|
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'spinner', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 500, 'step' => 5, 'unit' => '元', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce'], 'help' => '在会员商品基础上优惠的金额,优惠后总金额不能为0元'],
|
||||||
|
[
|
||||||
|
'key' => 'vip_1_renew_product', 'label' => '一级会员续费', 'type' => 'group', 'max' => 8,
|
||||||
|
'button_title' => '添加续费商品', 'default' => [
|
||||||
|
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||||
|
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||||
|
'help' => '一级会员续费的商品选项',
|
||||||
|
...self::vip_product_group_options(),
|
||||||
|
'fields' => self::vip_product_fields(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'vip_2_renew_product', 'label' => '二级会员续费', 'type' => 'group', 'max' => 8,
|
||||||
|
'button_title' => '添加续费商品', 'default' => [
|
||||||
|
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||||
|
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||||
|
],
|
||||||
|
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||||
|
'help' => '二级会员续费的商品选项',
|
||||||
|
...self::vip_product_group_options(),
|
||||||
|
'fields' => self::vip_product_fields(),
|
||||||
|
],
|
||||||
]],
|
]],
|
||||||
['key' => 'upgrade', 'title' => '会员升级', 'fields' => [
|
['key' => 'upgrade', 'title' => '会员升级', 'fields' => [
|
||||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]],
|
['key' => 'vip_upgrade_desc', 'label' => '升级介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '升级VIP会员,享更多会员权益', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '升级会员的一句话简介,支持HTML'],
|
||||||
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题'],
|
[
|
||||||
|
'key' => 'vip_upgrade_product', 'label' => '升级价格', 'type' => 'accordion', 'default' => [
|
||||||
|
'month_to_month_s' => true, 'unit_price' => '10.00', 'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'unit_tag_class' => 'jb-yellow',
|
||||||
|
'jump_s' => false, 'jump_price' => '199.00', 'jump_show_price' => '299.00', 'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'jump_tag_class' => 'jb-red',
|
||||||
|
'permanent_price' => '199.00', 'permanent_show_price' => '299.00', 'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'permanent_tag_class' => 'jb-red',
|
||||||
|
],
|
||||||
|
'dependsOn' => ['key' => 'vip_upgrade', 'value' => true],
|
||||||
|
'help' => '会员升级的商品选项',
|
||||||
|
'sections' => [
|
||||||
|
['title' => '月费会员升级月费会员', 'keys' => ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class']],
|
||||||
|
['title' => '月费会员升级永久会员', 'keys' => ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class']],
|
||||||
|
['title' => '永久会员升级永久会员', 'keys' => ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class']],
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
['key' => '_upgrade_unit_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价'],
|
||||||
|
['key' => 'month_to_month_s', 'label' => '月费会员升级月费会员', 'type' => 'switch', 'default' => true],
|
||||||
|
['key' => 'unit_price', 'label' => '天单价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true]],
|
||||||
|
['key' => 'unit_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'rows' => 1, 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||||
|
['key' => 'unit_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['all' => [['key' => 'month_to_month_s', 'value' => true], ['key' => 'unit_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||||
|
['key' => '_upgrade_jump_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项'],
|
||||||
|
['key' => 'jump_s', 'label' => '月费会员升级永久会员', 'type' => 'switch', 'default' => false],
|
||||||
|
['key' => 'jump_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '月费会员升级永久会员的价格'],
|
||||||
|
['key' => 'jump_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '显示在执行价格前面,并划掉'],
|
||||||
|
['key' => 'jump_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||||
|
['key' => 'jump_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['all' => [['key' => 'jump_s', 'value' => true], ['key' => 'jump_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||||
|
['key' => 'permanent_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'help' => '永久会员升级永久会员的价格'],
|
||||||
|
['key' => 'permanent_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||||
|
['key' => 'permanent_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||||
|
['key' => 'permanent_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['key' => 'permanent_tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||||
|
],
|
||||||
|
],
|
||||||
]],
|
]],
|
||||||
]],
|
]],
|
||||||
]],
|
]],
|
||||||
['key' => 'points-exchange', 'title' => '积分兑换会员', 'fields' => [
|
['key' => 'points-exchange', 'title' => '积分兑换会员', 'fields' => [
|
||||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费'],
|
||||||
['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;普通用户可开通,已有会员可续费同等级或升级更高等级)'],
|
[
|
||||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]],
|
'key' => 'pay_vip_points_exchange_product', 'label' => '积分兑换明细', 'type' => 'group', 'max' => 16,
|
||||||
|
'button_title' => '添加积分兑换商品', 'default' => [
|
||||||
|
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||||
|
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||||
|
],
|
||||||
|
'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true],
|
||||||
|
'help' => '每一行为一个可兑换的会员套餐',
|
||||||
|
'collapsible' => true,
|
||||||
|
'defaultCollapsed' => true,
|
||||||
|
'listPreview' => ['prefix' => '积分:', 'titleKey' => 'points', 'descKey' => 'time', 'descUnitKey' => 'unit'],
|
||||||
|
'fields' => [
|
||||||
|
['key' => 'points', 'label' => '使用积分', 'type' => 'number', 'default' => 100, 'min' => 0, 'unit' => '积分'],
|
||||||
|
['key' => 'level', 'label' => '兑换会员', 'type' => 'radio', 'default' => 1, 'inline' => true, 'options' => [
|
||||||
|
['label' => '一级会员', 'value' => 1],
|
||||||
|
['label' => '二级会员', 'value' => 2],
|
||||||
|
]],
|
||||||
|
['key' => 'time', 'label' => '兑换时长', 'type' => 'input', 'default' => '', 'help' => '填 Permanent 为永久'],
|
||||||
|
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'day', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'neq', 'value' => 'Permanent'], 'options' => [
|
||||||
|
['label' => '天', 'value' => 'day'],
|
||||||
|
['label' => '月', 'value' => 'month'],
|
||||||
|
]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '用户使用积分兑换时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||||
|
]],
|
||||||
|
['key' => 'modal-copy', 'title' => '界面文案', 'fields' => [
|
||||||
|
['key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]], 'help' => '显示在开通界面顶部的一句话简介,支持HTML'],
|
||||||
|
['key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>', 'rows' => 4, 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -358,6 +421,8 @@ class SchemaPayment {
|
|||||||
['key' => 'pay_rebate_poster_img', 'label' => '推广海报顶部图片', 'type' => 'media', 'default' => '', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]],
|
['key' => 'pay_rebate_poster_img', 'label' => '推广海报顶部图片', 'type' => 'media', 'default' => '', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]],
|
||||||
['key' => 'pay_rebate_poster_title', 'label' => '推广海报标题(20-30个字)', 'type' => 'input', 'default' => '发现一个精品网站,快来看一看', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]],
|
['key' => 'pay_rebate_poster_title', 'label' => '推广海报标题(20-30个字)', 'type' => 'input', 'default' => '发现一个精品网站,快来看一看', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]],
|
||||||
['key' => 'pay_rebate_poster_desc', 'label' => '推广海报简介(40-60个字)', 'type' => 'input', 'default' => '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]],
|
['key' => 'pay_rebate_poster_desc', 'label' => '推广海报简介(40-60个字)', 'type' => 'input', 'default' => '网站有超多精品资源和优惠福利,扫码进入网站还可享受诸多优惠哦', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true]],
|
||||||
|
['key' => 'pay_rebate_poster_banner', 'label' => '海报顶部备用图片', 'type' => 'media', 'default' => '', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true], 'help' => '顶部主图加载失败时依次回退到这里,再回退到主题内置图,避免海报顶部出现空白。'],
|
||||||
|
['key' => 'pay_rebate_poster_desc_bottom', 'label' => '海报底部文案', 'type' => 'input', 'default' => '扫描二维码阅读全文', 'dependsOn' => ['key' => 'pay_rebate_poster_s', 'value' => true], 'help' => '画在二维码下方居中位置,留空则使用默认文案。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'discount', 'title' => '推广让利标签', 'fields' => [
|
['key' => 'discount', 'title' => '推广让利标签', 'fields' => [
|
||||||
['key' => 'discount_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<p><b>推广让利标签配置详解:</b></p><ul style="margin:6px 0 0 18px;line-height:1.9"><li>可使用的变量:折扣 <code>%discount%</code>、推荐人姓名 <code>%referrer_name%</code></li><li>使用变量示例:<code>%referrer_name%推荐购买 下单再减%discount%元</code></li></ul>', 'dependsOn' => ['key' => 'pay_rebate_s', 'value' => true]],
|
['key' => 'discount_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<p><b>推广让利标签配置详解:</b></p><ul style="margin:6px 0 0 18px;line-height:1.9"><li>可使用的变量:折扣 <code>%discount%</code>、推荐人姓名 <code>%referrer_name%</code></li><li>使用变量示例:<code>%referrer_name%推荐购买 下单再减%discount%元</code></li></ul>', 'dependsOn' => ['key' => 'pay_rebate_s', 'value' => true]],
|
||||||
@@ -506,7 +571,48 @@ class SchemaPayment {
|
|||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* 售后策略
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
'after_sale' => [
|
||||||
|
'key' => 'after_sale',
|
||||||
|
'title' => '售后策略',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'option_key' => 'aksrt_settings',
|
||||||
|
'groups' => [
|
||||||
|
['key' => 'main', 'title' => '售后功能', 'icon' => 'fa fas fa-undo-alt', 'fields' => [
|
||||||
|
['key' => 'after_sale_s', 'label' => '售后功能', 'type' => 'switch', 'default' => true, 'help' => '开启后用户可以申请售后;关闭后不再处理售后,前台「我的订单」不再显示申请售后按钮'],
|
||||||
|
['key' => 'after_sale_time_limit', 'label' => '申请售后时效(天)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 3650, 'help' => '订单支付后多少天内可以申请售后,超过时效将无法申请;0 表示不限制'],
|
||||||
|
['key' => 'after_sale_expired_text', 'label' => '超过时效提示', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<div>抱歉,您的订单已超过售后申请时效</div>', 'rows' => 3, 'help' => '订单超过售后时效后,用户点击申请售后时弹窗提示的内容(支持HTML代码,请注意代码规范)'],
|
||||||
|
]],
|
||||||
|
['key' => 'form', 'title' => '申请表单', 'icon' => 'fa fas fa-file-alt', 'fields' => [
|
||||||
|
['key' => 'after_sale_reasons', 'label' => '预置原因', 'type' => 'group', 'button_title' => '添加原因', 'help' => '用户申请售后时可选择的原因', 'default' => [['t' => '质量问题'], ['t' => '假冒品牌'], ['t' => '其他原因']], 'fields' => [
|
||||||
|
['key' => 't', 'label' => '原因', 'type' => 'input', 'default' => ''],
|
||||||
|
]],
|
||||||
|
['key' => 'after_sale_desc', 'label' => '申请售后说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 5, 'help' => '用户申请售后时显示的内容(支持HTML代码,请注意代码规范)'],
|
||||||
|
['key' => 'after_sale_materials', 'label' => '申请所需材料', 'type' => 'group', 'button_title' => '添加材料', 'help' => '用户提交售后申请时需要提供的资料,如联系邮箱、联系电话;此处的信息收集,仅用于售后联系与审核', 'default' => [['t' => '联系邮箱'], ['t' => '联系电话']], 'fields' => [
|
||||||
|
['key' => 't', 'label' => '资料项', 'type' => 'input', 'default' => ''],
|
||||||
|
]],
|
||||||
|
['key' => 'after_sale_reason_placeholder', 'label' => '申请说明占位文案', 'type' => 'input', 'default' => '必填,请您详细填写申请原因', 'help' => '前台申请售后弹窗中「申请说明」输入框的提示文字'],
|
||||||
|
['key' => 'after_sale_img_s', 'label' => '申请售后可上传图片', 'type' => 'switch', 'default' => true, 'help' => '开启后申请售后弹窗中出现图片上传入口(最多上传3张)'],
|
||||||
|
]],
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||||
|
*
|
||||||
|
* @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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,4 +656,92 @@ class SchemaPayment {
|
|||||||
}
|
}
|
||||||
return $options;
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员商品/续费商品的统一字段集(参考主题 CFS_Module::vip_product 1:1)。
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private static function vip_product_fields(): array {
|
||||||
|
return [
|
||||||
|
['key' => 'price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '699.00', 'min' => 0, 'suffix' => '元', 'help' => '会员的实际购买价格'],
|
||||||
|
['key' => 'show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '999.00', 'min' => 0, 'suffix' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||||
|
['key' => 'tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||||
|
['key' => 'tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['key' => 'tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||||
|
['key' => 'time', 'label' => '会员有效时间', 'type' => 'spinner', 'default' => 3, 'min' => 0, 'max' => 3600, 'step' => 1, 'help' => '开通会员的时长。填0则为永久会员'],
|
||||||
|
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'month', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'truthy'], 'options' => [
|
||||||
|
['label' => '天', 'value' => 'day'],
|
||||||
|
['label' => '月', 'value' => 'month'],
|
||||||
|
]],
|
||||||
|
['key' => '_time_permanent_hint', 'label' => '', 'type' => 'alert', 'alertType' => 'success', 'ui_only' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'eq', 'value' => 0], 'content' => '会员有效时间已设置为:永久会员'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员等级 Tab 的字段集(参考主题 CFS_Module::vip_tab 1:1,显示名称并入 Tab)。
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private static function vip_level_tab_fields(int $level): array {
|
||||||
|
$dep = ['key' => 'pay_user_vip_' . $level . '_s', 'value' => true];
|
||||||
|
$equity = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||||
|
return [
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_name', 'label' => '会员显示名称', 'type' => 'input', 'sanitize_type' => 'html', 'default' => $level === 1 ? '黄金会员' : '钻石会员', 'dependsOn' => $dep, 'help' => '会员名称,支持HTML'],
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_desc', 'label' => '一句话简介', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '尊享专属特权', 'dependsOn' => $dep, 'help' => '支持HTML'],
|
||||||
|
['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => $equity, 'rows' => 4, 'dependsOn' => $dep, 'help' => '支持安全HTML,建议使用 <li> 列表项'],
|
||||||
|
['key' => 'vip_' . $level . '_product_s', 'label' => '购买会员', 'type' => 'switch', 'default' => true, 'dependsOn' => $dep, 'help' => '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能'],
|
||||||
|
[
|
||||||
|
'key' => 'vip_' . $level . '_product', 'label' => '会员商品', 'type' => 'group', 'max' => 8,
|
||||||
|
'button_title' => '添加会员商品', 'default' => self::vip_product_defaults(),
|
||||||
|
...self::vip_product_group_options(),
|
||||||
|
'dependsOn' => ['all' => [$dep, ['key' => 'vip_' . $level . '_product_s', 'value' => true]]],
|
||||||
|
'help' => '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||||
|
'fields' => self::vip_product_fields(),
|
||||||
|
],
|
||||||
|
['key' => 'vip_' . $level . '_img_icon', 'label' => '会员图标', 'type' => 'upload', 'default' => '', 'dependsOn' => $dep, 'help' => '留空使用内置 vip-' . $level . '.svg 图标;建议尺寸300x300'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private static function vip_product_defaults(): array {
|
||||||
|
return [
|
||||||
|
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||||
|
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签颜色调色板(参考主题 zib_palette(array(), array('b','jb')))。
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,string>>
|
||||||
|
*/
|
||||||
|
private static function vip_tag_palette(): array {
|
||||||
|
$colors = [
|
||||||
|
'b-red' => '#f74b3d',
|
||||||
|
'b-yellow' => '#f3920a',
|
||||||
|
'b-cyan' => '#08c4c1',
|
||||||
|
'b-blue' => '#0a8cf3',
|
||||||
|
'b-green' => '#1fd05a',
|
||||||
|
'b-purple' => '#c133f5',
|
||||||
|
'b-black' => '#121517',
|
||||||
|
'jb-red' => 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)',
|
||||||
|
'jb-pink' => 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)',
|
||||||
|
'jb-yellow' => 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)',
|
||||||
|
'jb-cyan' => 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)',
|
||||||
|
'jb-blue' => 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)',
|
||||||
|
'jb-green' => 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)',
|
||||||
|
'jb-purple' => 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)',
|
||||||
|
'jb-vip1' => 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)',
|
||||||
|
'jb-vip2' => 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)',
|
||||||
|
];
|
||||||
|
$options = [];
|
||||||
|
foreach ($colors as $value => $css) {
|
||||||
|
$options[] = ['label' => $css, 'value' => $value];
|
||||||
|
}
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loaded 100 of 188 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user