feat(payment): 支付&付费菜单按参考主题重构并新增提现策略页

- 子菜单重组为知识付费/余额充值/VIP会员/创作分成/推广返佣/收款接口/提现策略
- 收款接口页 1:1 复刻 Zibll 双下拉结构,仅保留支付宝/微信/PayPal/Stripe/易支付
- 订单前缀/流水号前缀/提现单号前缀配置保留并移入收款接口页
- 知识付费/推广返佣按参考主题全字段复刻,余额充值并入充值卡配置
- 新增 tabbed/between_number/page_select 表单控件及后端 pages 搜索源
- 清理旧 SPA 组件、无消费者 REST 端点与废弃配置键(不写双路兼容)
- 更新存量契约测试至新 schema 文件
This commit is contained in:
2026-09-16 19:18:28 +08:00
parent fb5cdf247d
commit a48007a44d
36 changed files with 2042 additions and 2273 deletions
@@ -142,6 +142,22 @@
@update:value="emit('update:modelValue', $event)"
/>
<!-- Page Select (async page list) -->
<n-select
v-else-if="schema.type === 'page_select'"
:value="modelValue"
:options="pageOptions"
:loading="pageLoading"
:placeholder="schema.placeholder || '选择页面'"
:multiple="schema.multiple === true"
:filterable="true"
:remote="true"
:clearable="schema.clearable !== false"
:disabled="schema.disabled"
@search="handlePageSearch"
@update:value="emit('update:modelValue', $event)"
/>
<!-- Radio group -->
<n-radio-group
v-else-if="schema.type === 'radio'"
@@ -332,6 +348,32 @@
/>
</div>
<!-- Between number (min/max pair, Zibll between_number) -->
<div v-else-if="schema.type === 'between_number'" class="aksrt-field__between">
<n-input-number
:value="betweenValue('min')"
:min="schema.min"
:max="schema.max"
:step="schema.step || 1"
:placeholder="schema.minPlaceholder || '最小'"
:disabled="schema.disabled"
style="flex: 1"
@update:value="updateBetween('min', $event)"
/>
<span class="aksrt-field__between-sep">—</span>
<n-input-number
:value="betweenValue('max')"
:min="schema.min"
:max="schema.max"
:step="schema.step || 1"
:placeholder="schema.maxPlaceholder || '最大'"
:disabled="schema.disabled"
style="flex: 1"
@update:value="updateBetween('max', $event)"
/>
<span v-if="schema.unit" class="aksrt-field__between-unit">{{ schema.unit }}</span>
</div>
<!-- Spacing (top right bottom left) -->
<div v-else-if="schema.type === 'spacing'" class="aksrt-field__spacing">
<div class="aksrt-field__spacing-label">{{ schema.spacingLabels?.[0] || '上' }}</div>
@@ -807,6 +849,9 @@ onMounted(() => {
if (props.schema.type === 'async_select') {
loadAsyncOptions()
}
if (props.schema.type === 'page_select') {
loadPages()
}
})
// ============ User Select (async search) ============
@@ -850,6 +895,31 @@ function handleUserSearch(query) {
}, 300)
}
// ============ Page Select (async page list) ============
const pageOptions = ref([])
const pageLoading = ref(false)
let pageSearchTimer = null
async function loadPages(query = '') {
pageLoading.value = true
try {
const selected = props.modelValue ? [props.modelValue] : []
const res = await api.searchPageDisplayOptions('pages', query, query ? [] : selected)
const items = res?.data || res || []
pageOptions.value = Array.isArray(items) ? items : []
} catch (e) {
console.warn('[AksField] page load failed:', e)
pageOptions.value = []
} finally {
pageLoading.value = false
}
}
function handlePageSearch(query) {
clearTimeout(pageSearchTimer)
pageSearchTimer = setTimeout(() => loadPages(query), 250)
}
const fieldClasses = computed(() => ({
'aksrt-field--disabled': props.schema.disabled,
'aksrt-field--inline': props.schema.inline,
@@ -870,6 +940,21 @@ function updateDimension(part, value) {
emit('update:modelValue', { ...current, [part]: value })
}
// ============ Between Number (min/max pair) ============
function betweenValue(part) {
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
? props.modelValue
: {}
return current[part] !== undefined ? current[part] : (props.schema.default?.[part] ?? 0)
}
function updateBetween(part, value) {
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
? props.modelValue
: {}
emit('update:modelValue', { ...current, [part]: value })
}
// ============ Spacing ============
function updateSpacing(part, value) {
const current = props.modelValue || {}
@@ -1115,6 +1200,25 @@ function confirmRepeaterModal() {
font-size: 14px;
}
/* Between number (min/max) */
.aksrt-field__between {
display: flex;
align-items: center;
gap: 8px;
max-width: 360px;
}
.aksrt-field__between-sep {
color: var(--aksrt-color-border-strong);
font-size: 14px;
}
.aksrt-field__between-unit {
color: var(--aksrt-color-muted);
font-size: 13px;
white-space: nowrap;
}
/* Spacing field */
.aksrt-field__spacing {
display: grid;
@@ -56,8 +56,33 @@
<template v-if="group.fields && !hasGroupSlot(group)">
<template v-for="field in group.fields" :key="field.key">
<template v-if="isFieldVisible(field)">
<!-- Tabbed: renders tabs whose sub-fields are flat top-level keys -->
<div
v-if="field.type === 'tabbed' && (field.tabs || []).length"
class="aksrt-form-renderer__tabbed"
>
<n-tabs type="line" animated>
<n-tab-pane
v-for="tab in field.tabs"
:key="tab.title || tab.key"
:name="tab.title || tab.key"
:tab="tab.title || tab.key"
>
<template v-for="sub in (tab.fields || [])" :key="sub.key">
<AksField
v-if="isFieldVisible(sub)"
:schema="sub"
:model-value="getFieldValue(sub.key)"
:error-message="errors?.[sub.key]"
@update:model-value="setFieldValue(sub.key, $event)"
@action="emit('field-action', $event)"
/>
</template>
</n-tab-pane>
</n-tabs>
</div>
<slot
v-if="field.type === 'custom'"
v-else-if="field.type === 'custom'"
:name="`field-${field.key}`"
v-bind="{ field, value: getFieldValue(field.key), setValue: (value) => setFieldValue(field.key, value), formData }"
>
@@ -268,6 +293,15 @@ const filteredSchema = computed(() => {
// Filter fields by label, key, or help text matching query
const matchingFields = group.fields.filter(field => {
if (groupMatch) return true
if (field.type === 'tabbed' && Array.isArray(field.tabs)) {
return (field.label || '').toLowerCase().includes(q)
|| field.tabs.some(tab => (tab.fields || []).some(sub =>
(sub.label || '').toLowerCase().includes(q)
|| (sub.key || '').toLowerCase().includes(q)
|| (sub.help || '').toLowerCase().includes(q)
|| (sub.description || '').toLowerCase().includes(q)
))
}
return (field.label || '').toLowerCase().includes(q)
|| (field.key || '').toLowerCase().includes(q)
|| (field.help || '').toLowerCase().includes(q)
@@ -318,6 +352,10 @@ const filteredSchema = computed(() => {
line-height: 1.6;
}
.aksrt-form-renderer__tabbed {
margin-bottom: 8px;
}
.aksrt-form-renderer__search {
min-width: 200px;
max-width: 280px;