Files
wordpress-theme-aksrt/assets/admin/src/components/form/AksField.vue
T
kate522 ee49d1628a feat(admin): list-modal UI for header icon cards repeater
Render icon cards as a compact list with edit/delete actions and
preview on the right; field data is edited in a modal. Opt-in via
schema ui=list-modal so other repeaters stay inline.
2026-07-15 17:27:23 +08:00

1157 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="aksrt-field" :class="fieldClasses">
<div 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="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'"
:value="modelValue"
@update:value="emit('update:modelValue', $event)"
/>
<!-- Input (text, email, url, password) -->
<n-input
v-else-if="schema.type === 'input' || schema.type === 'text'"
:value="modelValue"
: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>
<!-- 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'"
: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 -->
<n-select
v-else-if="schema.type === 'select'"
:value="modelValue"
:options="normalizedOptions"
:placeholder="schema.placeholder || '请选择'"
:multiple="schema.multiple"
:clearable="schema.clearable !== false"
:disabled="schema.disabled"
@update:value="emit('update:modelValue', $event)"
/>
<!-- 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)"
/>
<!-- 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'"
:model-value="modelValue"
:accept="schema.accept"
:placeholder="schema.placeholder || '选择图片'"
@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.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 || 'fas fa-image'" :size="20" />
</div>
<span class="aksrt-field__image-select-label">{{ opt.label }}</span>
</div>
</div>
<!-- Repeater (array of objects) — inline expand (default) -->
<div v-else-if="schema.type === 'repeater' && 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"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fas fa-trash" :size="12" />
</n-button>
</div>
<div class="aksrt-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
: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"
@click="addRepeaterItem"
>
<FaIcon icon="fas fa-plus" :size="12" style="margin-right: 6px" />
{{ schema.addText || '添加一项' }}
</n-button>
</div>
<!-- Repeater list-modal: compact list + modal editor -->
<div v-else-if="schema.type === 'repeater' && 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="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="fas fa-arrow-down" :size="12" />
</n-button>
<n-button
size="tiny"
quaternary
type="primary"
:disabled="schema.disabled"
@click="openRepeaterModal(index)"
>
<FaIcon icon="fas fa-edit" :size="12" />
编辑
</n-button>
<n-button
size="tiny"
quaternary
type="error"
:disabled="schema.disabled"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fas fa-trash" :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="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="fas fa-inbox" :size="18" />
<span>{{ schema.emptyText || '暂无数据' }}</span>
</div>
<n-button
size="small"
dashed
block
:disabled="schema.disabled"
@click="openRepeaterModal(-1)"
>
<FaIcon icon="fas fa-plus" :size="12" style="margin-right: 6px" />
{{ 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 || [])"
: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, slider, code, alert, 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 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'])
/** 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 { label: opt.label ?? opt.name ?? opt.value, value: opt.value ?? opt.label ?? opt.name }
}
return { label: String(opt), value: opt }
})
})
// ============ 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()
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()
}
})
// ============ 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() {
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
current.push(createRepeaterItem())
emit('update:modelValue', current)
}
function removeRepeaterItem(index) {
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 (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__label-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.aksrt-field__label {
font-size: 13px;
font-weight: 500;
color: #555;
}
.aksrt-field__required {
color: #d03050;
margin-left: 2px;
}
.aksrt-field__help-icon {
color: #c0c4cc;
cursor: help;
transition: color 0.15s;
}
.aksrt-field__help-icon:hover {
color: #1890ff;
}
.aksrt-field__desc {
margin: -2px 0 8px;
font-size: 12px;
color: #8a909c;
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: #c0c4cc;
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: #8a909c;
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: #8a909c;
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 #eef1f6;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
min-width: 80px;
}
.aksrt-field__image-select-item:hover {
border-color: #1890ff;
}
.aksrt-field__image-select-item--active {
border-color: #1890ff;
background: #e6f7ff;
}
.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: #f5f7fb;
border-radius: 4px;
color: #c0c4cc;
}
.aksrt-field__image-select-label {
font-size: 11px;
color: #555;
text-align: center;
}
/* Repeater */
.aksrt-field__repeater {
display: flex;
flex-direction: column;
gap: 12px;
}
.aksrt-field__repeater-item {
border: 1px solid #eef1f6;
border-radius: 8px;
padding: 12px;
background: #fafbfc;
}
.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: #1890ff;
}
.aksrt-field__repeater-fields {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Content field */
.aksrt-field__content {
font-size: 13px;
color: #555;
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;
}
}
/* 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 #eef1f6;
border-radius: 8px;
background: #fafbfc;
}
.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: #eef3ff;
color: #3b82f6;
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: #1f2329;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-field__repeater-list-desc {
margin-top: 2px;
font-size: 12px;
color: #8a909c;
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: #c0c4cc;
font-size: 13px;
border: 1px dashed #e5e7eb;
border-radius: 8px;
background: #fcfcfd;
}
.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 #eef1f6;
}
</style>