feat(payment): 支付&付费菜单按参考主题重构并新增提现策略页
- 子菜单重组为知识付费/余额充值/VIP会员/创作分成/推广返佣/收款接口/提现策略 - 收款接口页 1:1 复刻 Zibll 双下拉结构,仅保留支付宝/微信/PayPal/Stripe/易支付 - 订单前缀/流水号前缀/提现单号前缀配置保留并移入收款接口页 - 知识付费/推广返佣按参考主题全字段复刻,余额充值并入充值卡配置 - 新增 tabbed/between_number/page_select 表单控件及后端 pages 搜索源 - 清理旧 SPA 组件、无消费者 REST 端点与废弃配置键(不写双路兼容) - 更新存量契约测试至新 schema 文件
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user