Files
wordpress-theme-aksrt/assets/admin/src/components/homepage/ModuleCanvas.vue
T

314 lines
8.5 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-module-canvas">
<div class="aksrt-module-canvas__toolbar">
<n-space>
<n-dropdown :options="addModuleOptions" trigger="click" @select="handleAddModule">
<n-button type="primary" size="small">
<template #icon><FaIcon icon="fas fa-plus" :size="12" /></template>
添加模块
</n-button>
</n-dropdown>
<n-button size="small" @click="$emit('pick-preset')">
<template #icon><FaIcon icon="fas fa-magic" :size="12" /></template>
应用预设
</n-button>
<n-button size="small" @click="$emit('export')">
<template #icon><FaIcon icon="fas fa-download" :size="12" /></template>
导出
</n-button>
<n-button size="small" @click="$emit('import')">
<template #icon><FaIcon icon="fas fa-upload" :size="12" /></template>
导入
</n-button>
</n-space>
</div>
<div v-if="modules.length === 0" class="aksrt-module-canvas__empty">
<FaIcon icon="fas fa-th-large" :size="32" color="#c0c4cc" />
<p>暂无首页模块,点击"添加模块"或"应用预设"开始配置。</p>
</div>
<div v-else class="aksrt-module-canvas__list">
<div
v-for="(mod, index) in modules"
:key="mod.id || index"
class="aksrt-module-canvas__item"
:class="{
'aksrt-module-canvas__item--selected': selectedIndex === index,
'aksrt-module-canvas__item--disabled': !mod.enabled,
'aksrt-module-canvas__item--dragging': dragIndex === index,
'aksrt-module-canvas__item--drag-over': dragOverIndex === index && dragOverIndex !== dragIndex,
}"
:draggable="true"
@click="$emit('select', index)"
@dragstart="handleDragStart(index, $event)"
@dragover.prevent="handleDragOver(index)"
@dragleave="handleDragLeave"
@drop.prevent="handleDrop(index)"
@dragend="handleDragEnd"
>
<div class="aksrt-module-canvas__item-handle" title="拖拽排序">
<FaIcon icon="fas fa-grip-vertical" :size="12" color="#c0c4cc" />
</div>
<div class="aksrt-module-canvas__item-icon">
<FaIcon :icon="getModuleIcon(mod.type)" :size="14" />
</div>
<div class="aksrt-module-canvas__item-info">
<span class="aksrt-module-canvas__item-label">{{ getModuleLabel(mod.type) }}</span>
<span v-if="mod.title" class="aksrt-module-canvas__item-title">{{ mod.title }}</span>
</div>
<div class="aksrt-module-canvas__item-actions">
<n-switch
:value="mod.enabled !== false"
size="small"
@update:value="(val) => $emit('toggle', index, val)"
@click.stop
/>
<n-button
size="tiny"
quaternary
title="复制"
@click.stop="$emit('copy', index)"
>
<FaIcon icon="fas fa-copy" :size="11" />
</n-button>
<n-button
size="tiny"
quaternary
type="error"
title="删除"
@click.stop="$emit('remove', index)"
>
<FaIcon icon="fas fa-trash" :size="11" />
</n-button>
<n-button
size="tiny"
quaternary
:disabled="index === 0"
title="上移"
@click.stop="$emit('move', index, -1)"
>
<FaIcon icon="fas fa-chevron-up" :size="11" />
</n-button>
<n-button
size="tiny"
quaternary
:disabled="index === modules.length - 1"
title="下移"
@click.stop="$emit('move', index, 1)"
>
<FaIcon icon="fas fa-chevron-down" :size="11" />
</n-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { MODULE_LIBRARY } from '@/utils/home-module-schema'
import FaIcon from '@/components/form/FaIcon.vue'
const props = defineProps({
modules: { type: Array, default: () => [] },
selectedIndex: { type: Number, default: -1 },
})
const emit = defineEmits(['select', 'add', 'remove', 'copy', 'toggle', 'move', 'pick-preset', 'export', 'import'])
// Drag-and-drop state
const dragIndex = ref(-1)
const dragOverIndex = ref(-1)
function handleDragStart(index, event) {
dragIndex.value = index
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', String(index))
}
function handleDragOver(index) {
if (dragIndex.value !== index) {
dragOverIndex.value = index
}
}
function handleDragLeave() {
dragOverIndex.value = -1
}
function handleDrop(targetIndex) {
if (dragIndex.value >= 0 && dragIndex.value !== targetIndex) {
const offset = targetIndex - dragIndex.value
emit('move', dragIndex.value, offset)
}
dragIndex.value = -1
dragOverIndex.value = -1
}
function handleDragEnd() {
dragIndex.value = -1
dragOverIndex.value = -1
}
const addModuleOptions = computed(() =>
MODULE_LIBRARY.map(m => ({
label: m.label,
key: m.type,
icon: () => {
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 h('i', { class: iconMap[m.icon] || 'fas fa-cube', style: 'font-size:12px;margin-right:6px;' })
},
}))
)
function getModuleIcon(type) {
const meta = MODULE_LIBRARY.find(m => m.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[meta?.icon] || 'fas fa-cube'
}
function getModuleLabel(type) {
const meta = MODULE_LIBRARY.find(m => m.type === type)
return meta?.label || type
}
function handleAddModule(type) {
emit('add', type)
}
</script>
<script>
import { h } from 'vue'
export default {
// h is used in addModuleOptions computed icon renderer
}
</script>
<style scoped>
.aksrt-module-canvas__toolbar {
margin-bottom: 12px;
}
.aksrt-module-canvas__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
border: 2px dashed #e0e0e0;
border-radius: 8px;
color: #999;
gap: 12px;
}
.aksrt-module-canvas__empty p {
margin: 0;
font-size: 13px;
}
.aksrt-module-canvas__list {
display: flex;
flex-direction: column;
gap: 4px;
}
.aksrt-module-canvas__item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #f0f0f0;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
background: #fff;
}
.aksrt-module-canvas__item:hover {
border-color: #d0d0d0;
background: #fafafa;
}
.aksrt-module-canvas__item--selected {
border-color: #1890ff;
background: #e6f4ff;
}
.aksrt-module-canvas__item--dragging {
opacity: 0.5;
border-style: dashed;
}
.aksrt-module-canvas__item--drag-over {
border-color: #1890ff;
border-width: 2px;
background: #f0f7ff;
}
.aksrt-module-canvas__item--disabled {
opacity: 0.5;
}
.aksrt-module-canvas__item-handle {
cursor: grab;
padding: 2px;
}
.aksrt-module-canvas__item-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: #f5f7fa;
color: #666;
flex-shrink: 0;
}
.aksrt-module-canvas__item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.aksrt-module-canvas__item-label {
font-size: 13px;
font-weight: 500;
color: #333;
}
.aksrt-module-canvas__item-title {
font-size: 11px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-module-canvas__item-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
</style>