1454 lines
43 KiB
Vue
1454 lines
43 KiB
Vue
<template>
|
||
<div class="aksrt-field" :class="fieldClasses">
|
||
<div v-if="schema.label" class="aksrt-field__label-row">
|
||
<label class="aksrt-field__label">
|
||
{{ schema.label }}
|
||
<span v-if="schema.required" class="aksrt-field__required">*</span>
|
||
</label>
|
||
<n-tooltip v-if="schema.help" trigger="hover">
|
||
<template #trigger>
|
||
<FaIcon icon="fa fas fa-question-circle" :size="13" class="aksrt-field__help-icon" />
|
||
</template>
|
||
{{ schema.help }}
|
||
</n-tooltip>
|
||
</div>
|
||
|
||
<p v-if="schema.description && !schema.help" class="aksrt-field__desc">
|
||
{{ schema.description }}
|
||
</p>
|
||
|
||
<!-- Switch -->
|
||
<n-switch
|
||
v-if="schema.type === 'switch' || schema.type === 'switcher'"
|
||
:value="modelValue"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Input (text, email, url, password) -->
|
||
<div v-else-if="schema.type === 'input' || schema.type === 'text' || schema.type === 'url'" class="aksrt-field__input-wrap">
|
||
<n-input
|
||
:value="secretDisplayValue"
|
||
:placeholder="schema.placeholder || ''"
|
||
:type="schema.inputType || 'text'"
|
||
:disabled="schema.disabled"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
>
|
||
<template v-if="schema.prefix" #prefix>{{ schema.prefix }}</template>
|
||
<template v-if="schema.suffix" #suffix>{{ schema.suffix }}</template>
|
||
</n-input>
|
||
<n-button
|
||
v-if="schema.secret && (modelValue === '***' || secretMarkedForClear)"
|
||
size="small"
|
||
secondary
|
||
:type="secretMarkedForClear ? 'warning' : 'error'"
|
||
@click="toggleSecretClear"
|
||
>
|
||
{{ secretMarkedForClear ? '取消清除' : '清除已配置密钥' }}
|
||
</n-button>
|
||
<span v-if="secretMarkedForClear" class="aksrt-field__secret-clear-hint">保存后将删除此密钥</span>
|
||
</div>
|
||
|
||
<!-- Textarea -->
|
||
<n-input
|
||
v-else-if="schema.type === 'textarea'"
|
||
:value="modelValue"
|
||
type="textarea"
|
||
:placeholder="schema.placeholder || ''"
|
||
:rows="schema.rows || 4"
|
||
:disabled="schema.disabled"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Number -->
|
||
<n-input-number
|
||
v-else-if="schema.type === 'number' || schema.type === 'spinner'"
|
||
:value="modelValue"
|
||
:min="schema.min"
|
||
:max="schema.max"
|
||
:step="schema.step || 1"
|
||
:placeholder="schema.placeholder || ''"
|
||
:disabled="schema.disabled"
|
||
style="width: 100%"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Select -->
|
||
<div v-else-if="schema.type === 'select'" class="aksrt-field__ordered-select">
|
||
<n-select
|
||
:value="modelValue"
|
||
:options="normalizedOptions"
|
||
:placeholder="schema.placeholder || '请选择'"
|
||
:multiple="schema.multiple"
|
||
:clearable="schema.clearable !== false"
|
||
:disabled="schema.disabled"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
<div v-if="schema.multiple && schema.sortable && Array.isArray(modelValue)" class="aksrt-field__selected-order">
|
||
<div v-for="(value, index) in modelValue" :key="value" class="aksrt-field__selected-order-item">
|
||
<span>{{ selectedOptionLabel(normalizedOptions, value) }}</span>
|
||
<span><n-button text size="tiny" :disabled="index === 0" @click="moveSelectedValue(index, -1)">↑</n-button><n-button text size="tiny" :disabled="index === modelValue.length - 1" @click="moveSelectedValue(index, 1)">↓</n-button></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="schema.type === 'async_select'" class="aksrt-field__ordered-select">
|
||
<n-select
|
||
:value="schema.multiple === true && !Array.isArray(modelValue) ? [] : modelValue"
|
||
:options="asyncOptions"
|
||
:loading="asyncLoading"
|
||
:placeholder="schema.placeholder || '输入关键词搜索内容'"
|
||
:multiple="schema.multiple === true"
|
||
filterable
|
||
remote
|
||
clearable
|
||
@search="handleAsyncSearch"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
<div v-if="schema.multiple && schema.sortable && Array.isArray(modelValue)" class="aksrt-field__selected-order">
|
||
<div v-for="(value, index) in modelValue" :key="value" class="aksrt-field__selected-order-item">
|
||
<span>{{ selectedOptionLabel(asyncOptions, value) }}</span>
|
||
<span><n-button text size="tiny" :disabled="index === 0" @click="moveSelectedValue(index, -1)">↑</n-button><n-button text size="tiny" :disabled="index === modelValue.length - 1" @click="moveSelectedValue(index, 1)">↓</n-button></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Taxonomy Select (async-loaded category/tag terms) -->
|
||
<n-select
|
||
v-else-if="schema.type === 'taxonomy_select'"
|
||
:value="modelValue"
|
||
:options="taxonomyOptions"
|
||
:loading="taxonomyLoading"
|
||
:placeholder="schema.placeholder || '请选择分类'"
|
||
:multiple="schema.multiple !== false"
|
||
:filterable="true"
|
||
:clearable="schema.clearable !== false"
|
||
:disabled="schema.disabled"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- User Select (async search) -->
|
||
<n-select
|
||
v-else-if="schema.type === 'user_select'"
|
||
:value="modelValue"
|
||
:options="userOptions"
|
||
:loading="userLoading"
|
||
:placeholder="schema.placeholder || '搜索用户'"
|
||
:multiple="schema.multiple === true"
|
||
:filterable="true"
|
||
:remote="true"
|
||
:clearable="schema.clearable !== false"
|
||
:disabled="schema.disabled"
|
||
@search="handleUserSearch"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Radio group -->
|
||
<n-radio-group
|
||
v-else-if="schema.type === 'radio'"
|
||
:value="modelValue"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
>
|
||
<n-space v-if="schema.buttonStyle === false">
|
||
<n-radio
|
||
v-for="opt in normalizedOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
:disabled="schema.disabled"
|
||
>
|
||
{{ opt.label }}
|
||
</n-radio>
|
||
</n-space>
|
||
<n-radio-button
|
||
v-else
|
||
v-for="opt in normalizedOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
:disabled="schema.disabled"
|
||
>
|
||
{{ opt.label }}
|
||
</n-radio-button>
|
||
</n-radio-group>
|
||
|
||
<!-- Checkbox group -->
|
||
<n-checkbox-group
|
||
v-else-if="schema.type === 'checkbox'"
|
||
:value="modelValue || []"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
>
|
||
<n-space>
|
||
<n-checkbox
|
||
v-for="opt in normalizedOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
:label="opt.label"
|
||
:disabled="schema.disabled"
|
||
/>
|
||
</n-space>
|
||
</n-checkbox-group>
|
||
|
||
<!-- Color picker -->
|
||
<n-color-picker
|
||
v-else-if="schema.type === 'color'"
|
||
:value="modelValue"
|
||
:modes="['hex']"
|
||
:show-alpha="schema.showAlpha || false"
|
||
:disabled="schema.disabled"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<div v-else-if="schema.type === 'palette'" class="aksrt-field__palette">
|
||
<button
|
||
v-for="opt in normalizedOptions"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="aksrt-field__palette-item"
|
||
:class="{ 'is-active': modelValue === opt.value }"
|
||
:style="{ background: opt.label }"
|
||
:title="opt.value"
|
||
@click="emit('update:modelValue', opt.value)"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Icon picker -->
|
||
<FaIconPicker
|
||
v-else-if="schema.type === 'icon'"
|
||
:model-value="modelValue"
|
||
:placeholder="schema.placeholder || '选择图标'"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Media upload -->
|
||
<AksMediaField
|
||
v-else-if="schema.type === 'media' || schema.type === 'upload'"
|
||
:model-value="modelValue"
|
||
:accept="schema.accept"
|
||
:placeholder="schema.placeholder || '选择图片'"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- WordPress image gallery (attachment ID array) -->
|
||
<AksGalleryField
|
||
v-else-if="schema.type === 'gallery'"
|
||
:model-value="Array.isArray(modelValue) ? modelValue : []"
|
||
:title="schema.label || '选择图片'"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Slider -->
|
||
<n-slider
|
||
v-else-if="schema.type === 'slider'"
|
||
:value="modelValue"
|
||
:min="schema.min || 0"
|
||
:max="schema.max || 100"
|
||
:step="schema.step || 1"
|
||
:disabled="schema.disabled"
|
||
:marks="schema.marks"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Code editor with syntax highlighting (CodeMirror 6) -->
|
||
<AksCodeEditor
|
||
v-else-if="schema.type === 'code'"
|
||
:model-value="modelValue"
|
||
:language="schema.language || 'html'"
|
||
:disabled="schema.disabled"
|
||
:rows="schema.rows || 12"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Alert/info content -->
|
||
<n-alert
|
||
v-else-if="schema.type === 'alert' || schema.type === 'notice'"
|
||
:type="schema.alertType || 'info'"
|
||
:title="schema.alertTitle"
|
||
:closable="false"
|
||
>
|
||
{{ schema.content || schema.alertContent || modelValue }}
|
||
</n-alert>
|
||
|
||
<!-- Pure HTML content (no form field, just display) -->
|
||
<div
|
||
v-else-if="schema.type === 'content'"
|
||
class="aksrt-field__content"
|
||
v-html="schema.content || modelValue || ''"
|
||
/>
|
||
|
||
<!-- Date picker -->
|
||
<n-date-picker
|
||
v-else-if="schema.type === 'date'"
|
||
:value="modelValue"
|
||
:type="schema.dateType || 'date'"
|
||
:disabled="schema.disabled"
|
||
style="width: 100%"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Time picker -->
|
||
<n-time-picker
|
||
v-else-if="schema.type === 'time'"
|
||
:value="modelValue"
|
||
:disabled="schema.disabled"
|
||
style="width: 100%"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Dimensions (width x height) -->
|
||
<div v-else-if="schema.type === 'dimensions'" class="aksrt-field__dimensions">
|
||
<n-input-number
|
||
:value="modelValue?.width"
|
||
:min="0"
|
||
:placeholder="schema.widthPlaceholder || '宽度'"
|
||
:disabled="schema.disabled"
|
||
style="flex: 1"
|
||
@update:value="updateDimension('width', $event)"
|
||
/>
|
||
<span class="aksrt-field__dimensions-sep">×</span>
|
||
<n-input-number
|
||
:value="modelValue?.height"
|
||
:min="0"
|
||
:placeholder="schema.heightPlaceholder || '高度'"
|
||
:disabled="schema.disabled"
|
||
style="flex: 1"
|
||
@update:value="updateDimension('height', $event)"
|
||
/>
|
||
<n-select
|
||
v-if="schema.unit"
|
||
:value="modelValue?.unit"
|
||
:options="dimensionUnitOptions"
|
||
:disabled="schema.disabled"
|
||
style="width: 80px"
|
||
@update:value="updateDimension('unit', $event)"
|
||
/>
|
||
</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>
|
||
<n-input-number
|
||
:value="modelValue?.top"
|
||
:min="0"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateSpacing('top', $event)"
|
||
/>
|
||
<div class="aksrt-field__spacing-label">{{ schema.spacingLabels?.[1] || '右' }}</div>
|
||
<n-input-number
|
||
:value="modelValue?.right"
|
||
:min="0"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateSpacing('right', $event)"
|
||
/>
|
||
<div class="aksrt-field__spacing-label">{{ schema.spacingLabels?.[2] || '下' }}</div>
|
||
<n-input-number
|
||
:value="modelValue?.bottom"
|
||
:min="0"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateSpacing('bottom', $event)"
|
||
/>
|
||
<div class="aksrt-field__spacing-label">{{ schema.spacingLabels?.[3] || '左' }}</div>
|
||
<n-input-number
|
||
:value="modelValue?.left"
|
||
:min="0"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateSpacing('left', $event)"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Background (color + image + position + repeat) -->
|
||
<div v-else-if="schema.type === 'background'" class="aksrt-field__background">
|
||
<div class="aksrt-field__background-row">
|
||
<label class="aksrt-field__sublabel">背景颜色</label>
|
||
<n-color-picker
|
||
:value="modelValue?.color"
|
||
:show-alpha="true"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateBackground('color', $event)"
|
||
/>
|
||
</div>
|
||
<div class="aksrt-field__background-row">
|
||
<label class="aksrt-field__sublabel">背景图片</label>
|
||
<AksMediaField
|
||
:model-value="modelValue?.image"
|
||
:accept="'image/*'"
|
||
:disabled="schema.disabled"
|
||
@update:model-value="updateBackground('image', $event)"
|
||
/>
|
||
</div>
|
||
<div class="aksrt-field__background-row">
|
||
<label class="aksrt-field__sublabel">图片尺寸</label>
|
||
<n-select
|
||
:value="modelValue?.size"
|
||
:options="bgSizeOptions"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateBackground('size', $event)"
|
||
/>
|
||
</div>
|
||
<div class="aksrt-field__background-row">
|
||
<label class="aksrt-field__sublabel">图片位置</label>
|
||
<n-select
|
||
:value="modelValue?.position"
|
||
:options="bgPositionOptions"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateBackground('position', $event)"
|
||
/>
|
||
</div>
|
||
<div class="aksrt-field__background-row">
|
||
<label class="aksrt-field__sublabel">图片重复</label>
|
||
<n-select
|
||
:value="modelValue?.repeat"
|
||
:options="bgRepeatOptions"
|
||
:disabled="schema.disabled"
|
||
@update:value="updateBackground('repeat', $event)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Image select (radio with image preview) -->
|
||
<div v-else-if="schema.type === 'image_select'" class="aksrt-field__image-select">
|
||
<div
|
||
v-for="opt in normalizedOptions"
|
||
:key="opt.value"
|
||
class="aksrt-field__image-select-item"
|
||
:class="{ 'aksrt-field__image-select-item--active': modelValue === opt.value }"
|
||
@click="!schema.disabled && emit('update:modelValue', opt.value)"
|
||
>
|
||
<img v-if="opt.image" :src="opt.image" :alt="opt.label" class="aksrt-field__image-select-img" />
|
||
<div v-else class="aksrt-field__image-select-placeholder">
|
||
<FaIcon :icon="opt.icon || 'fa fas fa-image'" :size="20" />
|
||
</div>
|
||
<span class="aksrt-field__image-select-label">{{ opt.label }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Fieldset (single nested object) -->
|
||
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
||
<AksField
|
||
v-for="field in visibleNestedFields"
|
||
:key="field.key"
|
||
:schema="field"
|
||
:model-value="fieldsetValue(field)"
|
||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||
/>
|
||
</div>
|
||
|
||
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
||
<n-input :value="modelValue?.url || ''" placeholder="https://" @update:value="updateLink('url', $event)" />
|
||
<n-select :value="modelValue?.target || ''" :options="linkTargetOptions" @update:value="updateLink('target', $event)" />
|
||
<n-input :value="modelValue?.text || ''" placeholder="链接文字(可选)" @update:value="updateLink('text', $event)" />
|
||
</div>
|
||
|
||
<div v-else-if="schema.type === 'sortable'" class="aksrt-field__sortable">
|
||
<div v-for="(item, index) in sortableItems" :key="item.key" class="aksrt-field__sortable-item">
|
||
<div class="aksrt-field__sortable-head">
|
||
<strong>{{ item.label }}</strong>
|
||
<span>
|
||
<n-button text size="tiny" :disabled="index === 0" @click="moveSortable(index, -1)">↑</n-button>
|
||
<n-button text size="tiny" :disabled="index === sortableItems.length - 1" @click="moveSortable(index, 1)">↓</n-button>
|
||
</span>
|
||
</div>
|
||
<div class="aksrt-field__sortable-body">
|
||
<AksField v-for="field in item.fields || []" :key="field.key" :schema="field" :model-value="modelValue?.[item.key]?.[field.key] ?? field.default" @update:model-value="updateSortable(item.key, field.key, $event)" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Repeater (array of objects) — inline expand (default) -->
|
||
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
|
||
<div
|
||
v-for="(item, index) in (modelValue || [])"
|
||
:key="item._id || index"
|
||
class="aksrt-field__repeater-item"
|
||
>
|
||
<div class="aksrt-field__repeater-header">
|
||
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
|
||
<n-button
|
||
size="tiny"
|
||
type="error"
|
||
text
|
||
:disabled="schema.disabled || reachedRepeaterMinimum"
|
||
@click="removeRepeaterItem(index)"
|
||
>
|
||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||
</n-button>
|
||
</div>
|
||
<div class="aksrt-field__repeater-fields">
|
||
<template v-for="field in (schema.fields || [])" :key="field.key">
|
||
<AksField
|
||
v-if="matchDependency(field.dependsOn, item)"
|
||
:schema="field"
|
||
:model-value="item[field.key]"
|
||
:disabled="schema.disabled"
|
||
@update:model-value="updateRepeaterItem(index, field.key, $event)"
|
||
/>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<n-button
|
||
size="small"
|
||
dashed
|
||
:disabled="schema.disabled || reachedRepeaterMaximum"
|
||
@click="addRepeaterItem"
|
||
>
|
||
<FaIcon icon="fa fas fa-plus" :size="12" style="margin-right: 6px" />
|
||
{{ schema.button_title || schema.addText || '添加一项' }}
|
||
</n-button>
|
||
</div>
|
||
|
||
<!-- Repeater list-modal: compact list + modal editor -->
|
||
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui === 'list-modal'" class="aksrt-field__repeater aksrt-field__repeater--list-modal">
|
||
<div v-if="(modelValue || []).length" class="aksrt-field__repeater-list">
|
||
<div
|
||
v-for="(item, index) in (modelValue || [])"
|
||
:key="item._id || index"
|
||
class="aksrt-field__repeater-list-item"
|
||
>
|
||
<div class="aksrt-field__repeater-list-actions">
|
||
<n-button
|
||
v-if="index > 0"
|
||
size="tiny"
|
||
quaternary
|
||
:disabled="schema.disabled"
|
||
@click="moveRepeaterItem(index, -1)"
|
||
>
|
||
<FaIcon icon="fa fas fa-arrow-up" :size="12" />
|
||
</n-button>
|
||
<n-button
|
||
v-if="index < (modelValue || []).length - 1"
|
||
size="tiny"
|
||
quaternary
|
||
:disabled="schema.disabled"
|
||
@click="moveRepeaterItem(index, 1)"
|
||
>
|
||
<FaIcon icon="fa fas fa-arrow-down" :size="12" />
|
||
</n-button>
|
||
<n-button
|
||
size="tiny"
|
||
quaternary
|
||
type="primary"
|
||
:disabled="schema.disabled"
|
||
@click="openRepeaterModal(index)"
|
||
>
|
||
<FaIcon icon="fa fas fa-edit" :size="12" />
|
||
编辑
|
||
</n-button>
|
||
<n-button
|
||
size="tiny"
|
||
quaternary
|
||
type="error"
|
||
:disabled="schema.disabled || reachedRepeaterMinimum"
|
||
@click="removeRepeaterItem(index)"
|
||
>
|
||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||
删除
|
||
</n-button>
|
||
</div>
|
||
<div class="aksrt-field__repeater-list-preview">
|
||
<div class="aksrt-field__repeater-list-icon" :class="getRepeaterPreviewIconClass(item)">
|
||
<span
|
||
v-if="getRepeaterPreviewCustomIcon(item)"
|
||
class="aksrt-field__repeater-list-icon-html"
|
||
v-html="getRepeaterPreviewCustomIcon(item)"
|
||
/>
|
||
<i v-else-if="getRepeaterPreviewIcon(item)" :class="getRepeaterPreviewIcon(item)" aria-hidden="true" />
|
||
<FaIcon v-else icon="fa fas fa-star" :size="16" />
|
||
</div>
|
||
<div class="aksrt-field__repeater-list-meta">
|
||
<div class="aksrt-field__repeater-list-title">
|
||
{{ getRepeaterPreviewTitle(item) }}
|
||
</div>
|
||
<div class="aksrt-field__repeater-list-desc">
|
||
{{ getRepeaterPreviewDesc(item) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="aksrt-field__repeater-empty">
|
||
<FaIcon icon="fa fas fa-inbox" :size="18" />
|
||
<span>{{ schema.emptyText || '暂无数据' }}</span>
|
||
</div>
|
||
<n-button
|
||
size="small"
|
||
dashed
|
||
block
|
||
:disabled="schema.disabled || reachedRepeaterMaximum"
|
||
@click="openRepeaterModal(-1)"
|
||
>
|
||
<FaIcon icon="fa fas fa-plus" :size="12" style="margin-right: 6px" />
|
||
{{ schema.button_title || schema.addText || '添加一项' }}
|
||
</n-button>
|
||
|
||
<n-modal
|
||
v-model:show="repeaterModalShow"
|
||
:mask-closable="false"
|
||
preset="card"
|
||
:title="repeaterEditingIndex >= 0 ? (schema.editText || '编辑项目') : (schema.addText || '添加项目')"
|
||
class="aksrt-modal-card"
|
||
style="max-width: 640px; width: 92vw;"
|
||
>
|
||
<div class="aksrt-field__repeater-modal-fields">
|
||
<AksField
|
||
v-for="field in (schema.fields || []).filter(field => matchDependency(field.dependsOn, repeaterDraft))"
|
||
:key="field.key"
|
||
:schema="field"
|
||
:model-value="repeaterDraft[field.key]"
|
||
@update:model-value="updateRepeaterDraft(field.key, $event)"
|
||
/>
|
||
</div>
|
||
<div class="aksrt-modal-footer">
|
||
<n-button @click="closeRepeaterModal">取消</n-button>
|
||
<n-button type="primary" @click="confirmRepeaterModal">确定</n-button>
|
||
</div>
|
||
</n-modal>
|
||
</div>
|
||
|
||
<!-- Custom slot -->
|
||
<slot v-else-if="schema.type === 'custom'" />
|
||
|
||
<!-- Fallback: text input -->
|
||
<n-input
|
||
v-else
|
||
:value="modelValue"
|
||
:placeholder="schema.placeholder || ''"
|
||
@update:value="emit('update:modelValue', $event)"
|
||
/>
|
||
|
||
<!-- Validation message -->
|
||
<p v-if="errorMessage" class="aksrt-field__error">{{ errorMessage }}</p>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
/**
|
||
* AksField — renders a single form field based on its schema definition.
|
||
*
|
||
* Supported types: switch, input/text, textarea, number, select, radio,
|
||
* checkbox, color, icon, media, gallery, slider, code, alert, fieldset, repeater (inline|list-modal), custom (slot).
|
||
*/
|
||
import { computed, ref, onMounted, watch } from 'vue'
|
||
import FaIcon from './FaIcon.vue'
|
||
import FaIconPicker from './FaIconPicker.vue'
|
||
import AksMediaField from './AksMediaField.vue'
|
||
import AksGalleryField from './AksGalleryField.vue'
|
||
import AksCodeEditor from './AksCodeEditor.vue'
|
||
import api from '@/api'
|
||
|
||
const props = defineProps({
|
||
/** Field schema definition */
|
||
schema: {
|
||
type: Object,
|
||
required: true,
|
||
// Expected shape: { key, label, type, default, options?, help?, ... }
|
||
},
|
||
/** Current field value */
|
||
modelValue: { default: null },
|
||
/** Error message to display */
|
||
errorMessage: { type: String, default: '' },
|
||
})
|
||
|
||
const emit = defineEmits(['update:modelValue'])
|
||
|
||
const secretMarkedForClear = computed(() => props.schema.secret && props.modelValue === '__AKSRT_CLEAR_SECRET__')
|
||
const secretDisplayValue = computed(() => secretMarkedForClear.value ? '' : props.modelValue)
|
||
|
||
function toggleSecretClear() {
|
||
emit('update:modelValue', secretMarkedForClear.value ? '***' : '__AKSRT_CLEAR_SECRET__')
|
||
}
|
||
|
||
const repeaterLength = computed(() => Array.isArray(props.modelValue) ? props.modelValue.length : 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))
|
||
|
||
function fieldsetValue(field) {
|
||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||
? props.modelValue
|
||
: {}
|
||
return Object.prototype.hasOwnProperty.call(current, field.key) ? current[field.key] : field.default
|
||
}
|
||
|
||
function updateFieldsetValue(key, value) {
|
||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||
? props.modelValue
|
||
: {}
|
||
emit('update:modelValue', { ...current, [key]: value })
|
||
}
|
||
|
||
/** Normalize options: support both string[] and {label, value}[] */
|
||
const normalizedOptions = computed(() => {
|
||
const opts = props.schema.options || []
|
||
return opts.map(opt => {
|
||
if (typeof opt === 'string') return { label: opt, value: opt }
|
||
if (typeof opt === 'object' && opt !== null) {
|
||
return { ...opt, label: opt.label ?? opt.name ?? opt.value, value: opt.value ?? opt.label ?? opt.name }
|
||
}
|
||
return { label: String(opt), value: opt }
|
||
})
|
||
})
|
||
|
||
const visibleNestedFields = computed(() => (props.schema.fields || []).filter(field => matchDependency(field.dependsOn, props.modelValue)))
|
||
const linkTargetOptions = [{ label: '当前窗口', value: '' }, { label: '当前窗口', value: '_self' }, { label: '新窗口', value: '_blank' }]
|
||
|
||
function matchDependency(rule, sourceValue) {
|
||
if (!rule) return true
|
||
const source = sourceValue && typeof sourceValue === 'object' ? sourceValue : {}
|
||
if (rule.all) return rule.all.every(item => matchDependency(item, source))
|
||
if (rule.any) return rule.any.some(item => matchDependency(item, source))
|
||
const value = source[rule.key]
|
||
if (rule.operator === 'truthy') return !!value
|
||
if (rule.operator === 'falsy') return !value
|
||
if (rule.operator === 'not_empty') return Array.isArray(value) ? value.length > 0 : value !== '' && value != null
|
||
if (rule.operator === 'empty') return Array.isArray(value) ? value.length === 0 : value === '' || value == null
|
||
if (rule.operator === 'eq') return value === rule.value
|
||
if (rule.operator === 'neq') return value !== rule.value
|
||
return rule.value === undefined || value === rule.value
|
||
}
|
||
|
||
function updateLink(key, value) {
|
||
emit('update:modelValue', { ...(props.modelValue || {}), [key]: value })
|
||
}
|
||
|
||
function selectedOptionLabel(options, value) {
|
||
return options.find(option => option.value === value)?.label || String(value)
|
||
}
|
||
|
||
function moveSelectedValue(index, delta) {
|
||
if (!Array.isArray(props.modelValue)) return
|
||
const target = index + delta
|
||
if (target < 0 || target >= props.modelValue.length) return
|
||
const values = [...props.modelValue]
|
||
;[values[index], values[target]] = [values[target], values[index]]
|
||
emit('update:modelValue', values)
|
||
}
|
||
|
||
const sortableItems = computed(() => {
|
||
const definitions = new Map((props.schema.items || []).map(item => [item.key, item]))
|
||
const order = Object.keys(props.modelValue || {}).filter(key => definitions.has(key))
|
||
for (const key of definitions.keys()) if (!order.includes(key)) order.push(key)
|
||
return order.map(key => definitions.get(key))
|
||
})
|
||
|
||
function updateSortable(itemKey, fieldKey, value) {
|
||
const current = props.modelValue || {}
|
||
emit('update:modelValue', { ...current, [itemKey]: { ...(current[itemKey] || {}), [fieldKey]: value } })
|
||
}
|
||
|
||
function moveSortable(index, delta) {
|
||
const items = [...sortableItems.value]
|
||
const target = index + delta
|
||
if (target < 0 || target >= items.length) return
|
||
;[items[index], items[target]] = [items[target], items[index]]
|
||
const current = props.modelValue || {}
|
||
emit('update:modelValue', Object.fromEntries(items.map(item => [item.key, current[item.key] || {}])))
|
||
}
|
||
|
||
const asyncOptions = ref([])
|
||
const asyncLoading = ref(false)
|
||
let asyncTimer = null
|
||
|
||
async function loadAsyncOptions(query = '') {
|
||
asyncLoading.value = true
|
||
try {
|
||
const selected = Array.isArray(props.modelValue) ? props.modelValue : (props.modelValue ? [props.modelValue] : [])
|
||
const response = await api.searchPageDisplayOptions(props.schema.source, query, query ? [] : selected)
|
||
asyncOptions.value = response?.data || []
|
||
} catch (error) {
|
||
asyncOptions.value = []
|
||
} finally {
|
||
asyncLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleAsyncSearch(query) {
|
||
clearTimeout(asyncTimer)
|
||
asyncTimer = setTimeout(() => loadAsyncOptions(query), 250)
|
||
}
|
||
|
||
|
||
|
||
// ============ Taxonomy Select (async) ============
|
||
const taxonomyOptions = ref([])
|
||
const taxonomyLoading = ref(false)
|
||
|
||
async function loadTaxonomyTerms() {
|
||
if (!props.schema.taxonomy) return
|
||
taxonomyLoading.value = true
|
||
try {
|
||
const res = await api.getCategories(props.schema.taxonomy || 'category')
|
||
const items = res?.data || res || []
|
||
taxonomyOptions.value = (Array.isArray(items) ? items : []).map(t => ({
|
||
label: t.name + (t.count ? ' (' + t.count + ')' : ''),
|
||
value: t.term_id,
|
||
}))
|
||
} catch (e) {
|
||
console.warn('[AksField] taxonomy load failed:', e)
|
||
taxonomyOptions.value = []
|
||
} finally {
|
||
taxonomyLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (props.schema.type === 'taxonomy_select') {
|
||
loadTaxonomyTerms()
|
||
}
|
||
if (props.schema.type === 'user_select') {
|
||
loadInitialUsers()
|
||
}
|
||
if (props.schema.type === 'async_select') {
|
||
loadAsyncOptions()
|
||
}
|
||
})
|
||
|
||
// ============ User Select (async search) ============
|
||
const userOptions = ref([])
|
||
const userLoading = ref(false)
|
||
let userSearchTimer = null
|
||
|
||
async function loadInitialUsers() {
|
||
userLoading.value = true
|
||
try {
|
||
const res = await api.searchUsers('', 20)
|
||
const items = res?.data || res || []
|
||
userOptions.value = (Array.isArray(items) ? items : []).map(u => ({
|
||
label: u.display_name + ' (' + u.user_login + ')',
|
||
value: u.id,
|
||
}))
|
||
} catch (e) {
|
||
console.warn('[AksField] user load failed:', e)
|
||
userOptions.value = []
|
||
} finally {
|
||
userLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleUserSearch(query) {
|
||
clearTimeout(userSearchTimer)
|
||
userSearchTimer = setTimeout(async () => {
|
||
userLoading.value = true
|
||
try {
|
||
const res = await api.searchUsers(query || '', 20)
|
||
const items = res?.data || res || []
|
||
userOptions.value = (Array.isArray(items) ? items : []).map(u => ({
|
||
label: u.display_name + ' (' + u.user_login + ')',
|
||
value: u.id,
|
||
}))
|
||
} catch (e) {
|
||
userOptions.value = []
|
||
} finally {
|
||
userLoading.value = false
|
||
}
|
||
}, 300)
|
||
}
|
||
|
||
const fieldClasses = computed(() => ({
|
||
'aksrt-field--disabled': props.schema.disabled,
|
||
'aksrt-field--inline': props.schema.inline,
|
||
[`aksrt-field--${props.schema.type}`]: !!props.schema.type,
|
||
}))
|
||
|
||
// ============ Dimensions ============
|
||
const dimensionUnitOptions = [
|
||
{ label: 'px', value: 'px' },
|
||
{ label: '%', value: '%' },
|
||
{ label: 'em', value: 'em' },
|
||
{ label: 'rem', value: 'rem' },
|
||
{ label: 'vw', value: 'vw' },
|
||
]
|
||
|
||
function updateDimension(part, value) {
|
||
const current = props.modelValue || {}
|
||
emit('update:modelValue', { ...current, [part]: value })
|
||
}
|
||
|
||
// ============ Spacing ============
|
||
function updateSpacing(part, value) {
|
||
const current = props.modelValue || {}
|
||
emit('update:modelValue', { ...current, [part]: value })
|
||
}
|
||
|
||
// ============ Background ============
|
||
const bgSizeOptions = [
|
||
{ label: '覆盖', value: 'cover' },
|
||
{ label: '包含', value: 'contain' },
|
||
{ label: '原始大小', value: 'auto' },
|
||
]
|
||
|
||
const bgPositionOptions = [
|
||
{ label: '居中', value: 'center center' },
|
||
{ label: '顶部居中', value: 'top center' },
|
||
{ label: '底部居中', value: 'bottom center' },
|
||
{ label: '左侧居中', value: 'center left' },
|
||
{ label: '右侧居中', value: 'center right' },
|
||
]
|
||
|
||
const bgRepeatOptions = [
|
||
{ label: '不重复', value: 'no-repeat' },
|
||
{ label: '平铺', value: 'repeat' },
|
||
{ label: '水平平铺', value: 'repeat-x' },
|
||
{ label: '垂直平铺', value: 'repeat-y' },
|
||
]
|
||
|
||
function updateBackground(part, value) {
|
||
const current = props.modelValue || {}
|
||
emit('update:modelValue', { ...current, [part]: value })
|
||
}
|
||
|
||
// ============ Repeater ============
|
||
let repeaterIdCounter = 0
|
||
const repeaterModalShow = ref(false)
|
||
const repeaterEditingIndex = ref(-1)
|
||
const repeaterDraft = ref({})
|
||
|
||
function createRepeaterItem() {
|
||
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
||
for (const field of (props.schema.fields || [])) {
|
||
if (field.default !== undefined) {
|
||
newItem[field.key] = field.default
|
||
} else if (field.type === 'switch') {
|
||
newItem[field.key] = false
|
||
} else if (field.type === 'checkbox' || field.type === 'repeater') {
|
||
newItem[field.key] = []
|
||
} else {
|
||
newItem[field.key] = ''
|
||
}
|
||
}
|
||
return newItem
|
||
}
|
||
|
||
function addRepeaterItem() {
|
||
if (reachedRepeaterMaximum.value) return
|
||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||
current.push(createRepeaterItem())
|
||
emit('update:modelValue', current)
|
||
}
|
||
|
||
function removeRepeaterItem(index) {
|
||
if (reachedRepeaterMinimum.value) return
|
||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||
current.splice(index, 1)
|
||
emit('update:modelValue', current)
|
||
}
|
||
|
||
function updateRepeaterItem(index, fieldKey, value) {
|
||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||
current[index] = { ...current[index], [fieldKey]: value }
|
||
emit('update:modelValue', current)
|
||
}
|
||
|
||
function moveRepeaterItem(index, delta) {
|
||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||
const target = index + delta
|
||
if (target < 0 || target >= current.length) return
|
||
const tmp = current[index]
|
||
current[index] = current[target]
|
||
current[target] = tmp
|
||
emit('update:modelValue', current)
|
||
}
|
||
|
||
function getRepeaterPreviewConfig() {
|
||
return props.schema.listPreview || {}
|
||
}
|
||
|
||
function getRepeaterPreviewIcon(item) {
|
||
const cfg = getRepeaterPreviewConfig()
|
||
const key = cfg.iconKey || 'icon'
|
||
return String(item?.[key] || '').trim()
|
||
}
|
||
|
||
function getRepeaterPreviewCustomIcon(item) {
|
||
const cfg = getRepeaterPreviewConfig()
|
||
const key = cfg.customIconKey || 'custom_icon'
|
||
return String(item?.[key] || '').trim()
|
||
}
|
||
|
||
function getRepeaterPreviewIconClass(item) {
|
||
const cfg = getRepeaterPreviewConfig()
|
||
const colorKey = cfg.iconClassKey || 'icon_class'
|
||
return String(item?.[colorKey] || '').trim()
|
||
}
|
||
|
||
function getRepeaterPreviewTitle(item) {
|
||
const cfg = getRepeaterPreviewConfig()
|
||
const key = cfg.titleKey || 'title'
|
||
const title = String(item?.[key] || '').trim()
|
||
return title || '未命名卡片'
|
||
}
|
||
|
||
function getRepeaterPreviewDesc(item) {
|
||
const cfg = getRepeaterPreviewConfig()
|
||
const key = cfg.descKey || 'desc'
|
||
const desc = String(item?.[key] || '').trim()
|
||
if (desc) return desc
|
||
const urlKey = cfg.urlKey || 'url'
|
||
const url = String(item?.[urlKey] || '').trim()
|
||
return url || '暂无描述'
|
||
}
|
||
|
||
function openRepeaterModal(index) {
|
||
if (index < 0 && reachedRepeaterMaximum.value) return
|
||
if (props.schema.disabled) return
|
||
if (index >= 0) {
|
||
const item = (props.modelValue || [])[index] || {}
|
||
repeaterDraft.value = { ...createRepeaterItem(), ...item }
|
||
repeaterEditingIndex.value = index
|
||
} else {
|
||
repeaterDraft.value = createRepeaterItem()
|
||
repeaterEditingIndex.value = -1
|
||
}
|
||
repeaterModalShow.value = true
|
||
}
|
||
|
||
function closeRepeaterModal() {
|
||
repeaterModalShow.value = false
|
||
repeaterEditingIndex.value = -1
|
||
repeaterDraft.value = {}
|
||
}
|
||
|
||
function updateRepeaterDraft(fieldKey, value) {
|
||
repeaterDraft.value = { ...repeaterDraft.value, [fieldKey]: value }
|
||
}
|
||
|
||
function confirmRepeaterModal() {
|
||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||
const item = { ...repeaterDraft.value }
|
||
if (!item._id) {
|
||
item._id = `rpt_${Date.now()}_${++repeaterIdCounter}`
|
||
}
|
||
if (repeaterEditingIndex.value >= 0 && repeaterEditingIndex.value < current.length) {
|
||
current[repeaterEditingIndex.value] = item
|
||
} else {
|
||
current.push(item)
|
||
}
|
||
emit('update:modelValue', current)
|
||
closeRepeaterModal()
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.aksrt-field {
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.aksrt-field:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.aksrt-field__input-wrap {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.aksrt-field__input-wrap .n-button {
|
||
justify-self: start;
|
||
}
|
||
|
||
.aksrt-field__secret-clear-hint {
|
||
color: var(--aksrt-color-warning, #d97706);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.aksrt-field__label-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.aksrt-field__label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--aksrt-color-text-secondary);
|
||
}
|
||
|
||
.aksrt-field__required {
|
||
color: #d03050;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.aksrt-field__help-icon {
|
||
color: var(--aksrt-color-border-strong);
|
||
cursor: help;
|
||
transition: color 0.15s;
|
||
}
|
||
|
||
.aksrt-field__help-icon:hover {
|
||
color: var(--aksrt-color-primary);
|
||
}
|
||
|
||
.aksrt-field__desc {
|
||
margin: -2px 0 8px;
|
||
font-size: 12px;
|
||
color: var(--aksrt-color-muted);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.aksrt-field__error {
|
||
margin: 4px 0 0;
|
||
font-size: 12px;
|
||
color: #d03050;
|
||
}
|
||
|
||
.aksrt-field--disabled {
|
||
opacity: 0.6;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Dimensions field */
|
||
.aksrt-field__dimensions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.aksrt-field__dimensions-sep {
|
||
color: var(--aksrt-color-border-strong);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Spacing field */
|
||
.aksrt-field__spacing {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto 1fr auto 1fr auto 1fr;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.aksrt-field__spacing-label {
|
||
font-size: 12px;
|
||
color: var(--aksrt-color-muted);
|
||
text-align: center;
|
||
}
|
||
|
||
/* Background field */
|
||
.aksrt-field__background {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.aksrt-field__background-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.aksrt-field__sublabel {
|
||
font-size: 12px;
|
||
color: var(--aksrt-color-muted);
|
||
min-width: 70px;
|
||
}
|
||
|
||
/* Image select */
|
||
.aksrt-field__image-select {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.aksrt-field__image-select-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px;
|
||
border: 2px solid var(--aksrt-color-border);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.aksrt-field__image-select-item:hover {
|
||
border-color: var(--aksrt-color-primary);
|
||
}
|
||
|
||
.aksrt-field__image-select-item--active {
|
||
border-color: var(--aksrt-color-primary);
|
||
background: var(--aksrt-color-primary-soft);
|
||
}
|
||
|
||
.aksrt-field__image-select-img {
|
||
width: 60px;
|
||
height: 60px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.aksrt-field__image-select-placeholder {
|
||
width: 60px;
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--aksrt-color-canvas);
|
||
border-radius: 4px;
|
||
color: var(--aksrt-color-border-strong);
|
||
}
|
||
|
||
.aksrt-field__image-select-label {
|
||
font-size: 11px;
|
||
color: var(--aksrt-color-text-secondary);
|
||
text-align: center;
|
||
}
|
||
|
||
.aksrt-field__palette {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.aksrt-field__palette-item {
|
||
width: 34px;
|
||
height: 26px;
|
||
padding: 0;
|
||
border: 3px solid #fff;
|
||
border-radius: 3px;
|
||
box-shadow: 0 0 0 1px var(--aksrt-color-border-strong);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.aksrt-field__palette-item.is-active {
|
||
box-shadow: 0 0 0 2px var(--aksrt-color-primary);
|
||
}
|
||
|
||
.aksrt-field__link {
|
||
display: grid;
|
||
grid-template-columns: minmax(220px, 1fr) 120px minmax(140px, .5fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.aksrt-field__sortable {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.aksrt-field__selected-order {
|
||
display: grid;
|
||
gap: 5px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.aksrt-field__selected-order-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 5px 9px;
|
||
border: 1px solid var(--aksrt-color-border);
|
||
border-radius: 3px;
|
||
background: var(--aksrt-color-canvas);
|
||
}
|
||
|
||
.aksrt-field__sortable-item {
|
||
border: 1px solid var(--aksrt-color-border);
|
||
background: var(--aksrt-color-surface);
|
||
}
|
||
|
||
.aksrt-field__sortable-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 12px;
|
||
background: var(--aksrt-color-canvas);
|
||
cursor: move;
|
||
}
|
||
|
||
.aksrt-field__sortable-body {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 16px;
|
||
padding: 12px;
|
||
}
|
||
|
||
/* Repeater */
|
||
.aksrt-field__fieldset {
|
||
padding: 14px;
|
||
border: 1px solid var(--aksrt-color-border);
|
||
border-radius: 8px;
|
||
background: var(--aksrt-color-canvas);
|
||
}
|
||
|
||
.aksrt-field__repeater {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.aksrt-field__repeater-item {
|
||
border: 1px solid var(--aksrt-color-border);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
background: var(--aksrt-color-canvas);
|
||
}
|
||
|
||
.aksrt-field__repeater-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.aksrt-field__repeater-index {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--aksrt-color-primary);
|
||
}
|
||
|
||
.aksrt-field__repeater-fields {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* Content field */
|
||
.aksrt-field__content {
|
||
font-size: 13px;
|
||
color: var(--aksrt-color-text-secondary);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Mobile */
|
||
@media (max-width: 768px) {
|
||
.aksrt-field__dimensions {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.aksrt-field__spacing {
|
||
grid-template-columns: auto 1fr;
|
||
}
|
||
|
||
.aksrt-field__background-row {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.aksrt-field__link,
|
||
.aksrt-field__sortable-body {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Repeater list-modal */
|
||
.aksrt-field__repeater--list-modal {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.aksrt-field__repeater-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--aksrt-color-border);
|
||
border-radius: 8px;
|
||
background: var(--aksrt-color-canvas);
|
||
}
|
||
|
||
.aksrt-field__repeater-list-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-preview {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--aksrt-color-primary-soft);
|
||
color: var(--aksrt-color-primary);
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-icon-html {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-icon-html :deep(svg),
|
||
.aksrt-field__repeater-list-icon-html :deep(img) {
|
||
width: 20px;
|
||
height: 20px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-meta {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--aksrt-color-text);
|
||
line-height: 1.35;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.aksrt-field__repeater-list-desc {
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
color: var(--aksrt-color-muted);
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.aksrt-field__repeater-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 22px 12px;
|
||
color: var(--aksrt-color-border-strong);
|
||
font-size: 13px;
|
||
border: 1px dashed var(--aksrt-color-border);
|
||
border-radius: 8px;
|
||
background: var(--aksrt-color-canvas);
|
||
}
|
||
|
||
.aksrt-field__repeater-modal-fields {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
max-height: min(70vh, 560px);
|
||
overflow: auto;
|
||
padding-right: 2px;
|
||
}
|
||
|
||
.aksrt-modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-top: 16px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--aksrt-color-border);
|
||
}
|
||
|
||
</style>
|