314 lines
8.5 KiB
Vue
314 lines
8.5 KiB
Vue
<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>
|