导入 AKSRT 主题开发源码
This commit is contained in:
1 parent
9681dd3d48
commit
1268a55dd0
595 files changed
+127705
No files matched your search
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<div class="aksrt-module-inspector">
|
||||
<div v-if="!module" class="aksrt-module-inspector__empty">
|
||||
<FaIcon icon="fas fa-mouse-pointer" :size="24" color="#c0c4cc" />
|
||||
<p>选择左侧模块查看和编辑配置</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="aksrt-module-inspector__header">
|
||||
<FaIcon :icon="getModuleIcon(module.type)" :size="16" />
|
||||
<span class="aksrt-module-inspector__type">{{ getModuleLabel(module.type) }}</span>
|
||||
</div>
|
||||
|
||||
<n-scrollbar style="max-height: calc(100vh - 320px);">
|
||||
<n-form
|
||||
:model="localConfig"
|
||||
label-placement="top"
|
||||
size="small"
|
||||
class="aksrt-module-inspector__form"
|
||||
>
|
||||
<!-- Common fields -->
|
||||
<n-form-item label="模块标题">
|
||||
<n-input
|
||||
:value="module.title || ''"
|
||||
placeholder="留空使用默认标题"
|
||||
@update:value="(val) => updateField('title', val)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="启用模块">
|
||||
<n-switch
|
||||
:value="module.enabled !== false"
|
||||
@update:value="(val) => updateField('enabled', val)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<!-- Type-specific fields from schema -->
|
||||
<template v-if="metaFields.length > 0">
|
||||
<n-form-item
|
||||
v-for="field in metaFields"
|
||||
:key="field.key"
|
||||
:label="field.label"
|
||||
>
|
||||
<!-- Switch -->
|
||||
<n-switch
|
||||
v-if="field.type === 'switch'"
|
||||
:value="localConfig[field.key] ?? field.default ?? false"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Number -->
|
||||
<n-input-number
|
||||
v-else-if="field.type === 'number'"
|
||||
:value="localConfig[field.key] ?? field.default ?? 0"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
style="width: 100%;"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Text -->
|
||||
<n-input
|
||||
v-else-if="field.type === 'text'"
|
||||
:value="localConfig[field.key] ?? field.default ?? ''"
|
||||
:placeholder="field.placeholder || ''"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Textarea -->
|
||||
<n-input
|
||||
v-else-if="field.type === 'textarea'"
|
||||
type="textarea"
|
||||
:value="localConfig[field.key] ?? field.default ?? ''"
|
||||
:rows="field.rows || 3"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Select -->
|
||||
<n-select
|
||||
v-else-if="field.type === 'select'"
|
||||
:value="localConfig[field.key] ?? field.default"
|
||||
:options="field.options || []"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Radio -->
|
||||
<n-radio-group
|
||||
v-else-if="field.type === 'radio'"
|
||||
:value="localConfig[field.key] ?? field.default"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
>
|
||||
<n-space>
|
||||
<n-radio
|
||||
v-for="opt in field.options"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
|
||||
<!-- Category (single) -->
|
||||
<n-select
|
||||
v-else-if="field.type === 'cat'"
|
||||
:value="localConfig[field.key] ?? field.default ?? 0"
|
||||
:options="categoryOptions"
|
||||
filterable
|
||||
placeholder="选择分类"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Categories (multiple) -->
|
||||
<n-select
|
||||
v-else-if="field.type === 'cats'"
|
||||
:value="localConfig[field.key] ?? field.default ?? []"
|
||||
:options="categoryOptions"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="选择分类"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
|
||||
<!-- Media Picker -->
|
||||
<div v-else-if="field.type === 'media'" class="aksrt-module-inspector__media">
|
||||
<div v-if="localConfig[field.key]" class="aksrt-module-inspector__media-preview">
|
||||
<img :src="localConfig[field.key]" alt="" />
|
||||
<n-button size="tiny" quaternary type="error" @click="updateConfig(field.key, '')">
|
||||
移除
|
||||
</n-button>
|
||||
</div>
|
||||
<n-input
|
||||
v-else
|
||||
:value="localConfig[field.key] ?? ''"
|
||||
placeholder="输入图片 URL 或点击下方选择"
|
||||
@update:value="(val) => updateConfig(field.key, val)"
|
||||
/>
|
||||
<n-button size="tiny" quaternary @click="openMediaPicker(field.key)">
|
||||
<template #icon><FaIcon icon="fas fa-image" :size="12" /></template>
|
||||
从媒体库选择
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- Repeater -->
|
||||
<div v-else-if="field.type === 'repeater'" class="aksrt-module-inspector__repeater">
|
||||
<div
|
||||
v-for="(item, idx) in (localConfig[field.key] ?? [])"
|
||||
:key="idx"
|
||||
class="aksrt-module-inspector__repeater-item"
|
||||
>
|
||||
<div class="aksrt-module-inspector__repeater-fields">
|
||||
<n-input
|
||||
v-for="sub in (field.fields || [])"
|
||||
:key="sub.key"
|
||||
:value="item[sub.key] ?? ''"
|
||||
:placeholder="sub.placeholder || sub.label || ''"
|
||||
size="small"
|
||||
@update:value="(val) => updateRepeaterItem(field.key, idx, sub.key, val)"
|
||||
/>
|
||||
</div>
|
||||
<n-button size="tiny" quaternary type="error" @click="removeRepeaterItem(field.key, idx)">
|
||||
<FaIcon icon="fas fa-times" :size="10" />
|
||||
</n-button>
|
||||
</div>
|
||||
<n-button size="tiny" quaternary @click="addRepeaterItem(field.key, field.fields)">
|
||||
<template #icon><FaIcon icon="fas fa-plus" :size="10" /></template>
|
||||
添加一项
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<n-text v-else depth="3" style="font-size: 12px;">
|
||||
此模块类型暂无可配置字段。
|
||||
</n-text>
|
||||
</n-form>
|
||||
</n-scrollbar>
|
||||
</template>
|
||||
|
||||
<!-- Image URL input modal (replaces native prompt) -->
|
||||
<n-modal v-model:show="showImageUrlInput" preset="card" title="输入图片 URL" style="max-width: 400px;" :bordered="false">
|
||||
<n-input v-model:value="imageUrlValue" placeholder="https://example.com/image.jpg" @keyup.enter="confirmImageUrl" />
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showImageUrlInput = false">取消</n-button>
|
||||
<n-button type="primary" @click="confirmImageUrl">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { MODULE_LIBRARY } from '@/utils/home-module-schema'
|
||||
import FaIcon from '@/components/form/FaIcon.vue'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
module: { type: Object, default: null },
|
||||
moduleIndex: { type: Number, default: -1 },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const categoryOptions = ref([])
|
||||
|
||||
const meta = computed(() => {
|
||||
if (!props.module) return null
|
||||
return MODULE_LIBRARY.find(m => m.type === props.module.type) || null
|
||||
})
|
||||
|
||||
const metaFields = computed(() => {
|
||||
return meta.value?.fields || []
|
||||
})
|
||||
|
||||
const localConfig = computed(() => {
|
||||
return props.module?.config || {}
|
||||
})
|
||||
|
||||
function getModuleIcon(type) {
|
||||
const m = MODULE_LIBRARY.find(x => x.type === type)
|
||||
const iconMap = {
|
||||
image: 'fas fa-image', rectangle: 'fas fa-square', folder: 'fas fa-folder',
|
||||
switcher: 'fas fa-columns', trophy: 'fas fa-trophy', clock: 'fas fa-clock',
|
||||
tag: 'fas fa-tag', link: 'fas fa-link', code: 'fas fa-code', fire: 'fas fa-fire',
|
||||
grid: 'fas fa-th', 'font-colors': 'fas fa-font', 'th-large': 'fas fa-th-large',
|
||||
swap: 'fas fa-arrows-alt-v', 'question-circle': 'fas fa-question-circle',
|
||||
table: 'fas fa-table', star: 'fas fa-star', handshake: 'fas fa-handshake',
|
||||
'shopping-bag': 'fas fa-shopping-bag',
|
||||
}
|
||||
return iconMap[m?.icon] || 'fas fa-cube'
|
||||
}
|
||||
|
||||
function getModuleLabel(type) {
|
||||
const m = MODULE_LIBRARY.find(x => x.type === type)
|
||||
return m?.label || type
|
||||
}
|
||||
|
||||
function updateField(key, value) {
|
||||
emit('update', props.moduleIndex, key, value)
|
||||
}
|
||||
|
||||
function updateConfig(key, value) {
|
||||
const newConfig = { ...(props.module?.config || {}), [key]: value }
|
||||
emit('update', props.moduleIndex, 'config', newConfig)
|
||||
}
|
||||
|
||||
// Image URL input state (replaces native prompt)
|
||||
const showImageUrlInput = ref(false)
|
||||
const imageUrlFieldKey = ref('')
|
||||
const imageUrlValue = ref('')
|
||||
|
||||
function openMediaPicker(fieldKey) {
|
||||
if (!window.wp || !window.wp.media) {
|
||||
// Fallback: show inline input instead of native prompt
|
||||
imageUrlFieldKey.value = fieldKey
|
||||
imageUrlValue.value = localConfig.value[fieldKey] || ''
|
||||
showImageUrlInput.value = true
|
||||
return
|
||||
}
|
||||
const frame = wp.media({
|
||||
title: '选择图片',
|
||||
button: { text: '使用此图片' },
|
||||
multiple: false,
|
||||
library: { type: 'image' },
|
||||
})
|
||||
frame.on('select', function () {
|
||||
const attachment = frame.state().get('selection').first().toJSON()
|
||||
updateConfig(fieldKey, attachment.url)
|
||||
})
|
||||
frame.open()
|
||||
}
|
||||
|
||||
function confirmImageUrl() {
|
||||
if (imageUrlValue.value.trim()) {
|
||||
updateConfig(imageUrlFieldKey.value, imageUrlValue.value.trim())
|
||||
}
|
||||
showImageUrlInput.value = false
|
||||
}
|
||||
|
||||
function addRepeaterItem(fieldKey, subFields) {
|
||||
const current = [...(props.module?.config?.[fieldKey] || [])]
|
||||
const newItem = {}
|
||||
;(subFields || []).forEach(f => { newItem[f.key] = f.default ?? '' })
|
||||
current.push(newItem)
|
||||
updateConfig(fieldKey, current)
|
||||
}
|
||||
|
||||
function removeRepeaterItem(fieldKey, index) {
|
||||
const current = [...(props.module?.config?.[fieldKey] || [])]
|
||||
current.splice(index, 1)
|
||||
updateConfig(fieldKey, current)
|
||||
}
|
||||
|
||||
function updateRepeaterItem(fieldKey, index, subKey, value) {
|
||||
const current = [...(props.module?.config?.[fieldKey] || [])]
|
||||
if (current[index]) {
|
||||
current[index] = { ...current[index], [subKey]: value }
|
||||
updateConfig(fieldKey, current)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await api.getHomepageCategories()
|
||||
const cats = res?.data || []
|
||||
categoryOptions.value = cats.map(c => ({
|
||||
label: `${c.name} (${c.count})`,
|
||||
value: c.term_id,
|
||||
}))
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
onMounted(loadCategories)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-module-inspector__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
color: #999;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__empty p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__type {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__form {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__media {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__media-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__media-preview img {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__repeater {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__repeater-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.aksrt-module-inspector__repeater-fields {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user