|
@@ -1,27 +1,55 @@
|
|
|
<template>
|
|
<template>
|
|
|
<el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false" @open="handleOpen">
|
|
<el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false" @open="handleOpen">
|
|
|
<div class="diamond-edit">
|
|
<div class="diamond-edit">
|
|
|
- <div v-for="(item, index) in items" :key="index"
|
|
|
|
|
- class="flex items-center gap-4 mb-4 p-3 border rounded bg-gray-50 relative group">
|
|
|
|
|
- <ImageUpload v-model="item.imgUrl" :limit="1" :isShowTip="false" />
|
|
|
|
|
-
|
|
|
|
|
- <div class="flex-1">
|
|
|
|
|
- <div class="flex items-center gap-2 mb-2">
|
|
|
|
|
- <el-select v-model="item.showCateId" filterable remote reserve-keyword placeholder="请搜索并选择书单"
|
|
|
|
|
- :remote-method="(query) => remoteMethod(query, index)" :loading="item.loading"
|
|
|
|
|
- @change="(val) => handleSelectChange(val, index)" clearable class="flex-1">
|
|
|
|
|
- <el-option v-for="opt in options[index] || []" :key="opt.value" :label="opt.label"
|
|
|
|
|
- :value="opt.value" :disabled="isOptionDisabled(opt.value, index)" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-input v-model="item.jumpUrl" placeholder="请输入跳转链接">
|
|
|
|
|
- <template #prepend>链接</template>
|
|
|
|
|
- </el-input>
|
|
|
|
|
- <div v-if="item.showCateId" class="text-xs text-gray-400 mt-1">
|
|
|
|
|
- 已绑定书单ID: {{ item.showCateId }}
|
|
|
|
|
|
|
+ <draggable v-model="items" item-key="_key" handle=".drag-handle" :animation="200" class="space-y-4">
|
|
|
|
|
+ <template #item="{ element: item, index }">
|
|
|
|
|
+ <div class="flex items-center gap-4 mb-4 p-3 border rounded bg-gray-50 relative group box-border">
|
|
|
|
|
+ <!-- Drag Handle -->
|
|
|
|
|
+ <div class="drag-handle cursor-move text-gray-400 hover:text-gray-600">
|
|
|
|
|
+ <el-icon :size="20">
|
|
|
|
|
+ <Rank />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <ImageUpload v-model="item.imgUrl" :limit="1" :isShowTip="false" />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex-1">
|
|
|
|
|
+ <div class="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <el-select v-model="item.showCateId" filterable remote reserve-keyword
|
|
|
|
|
+ placeholder="请搜索并选择书单" :remote-method="(query) => remoteMethod(query, index)"
|
|
|
|
|
+ :loading="item.loading" @change="(val) => handleSelectChange(val, index)" clearable
|
|
|
|
|
+ class="flex-1">
|
|
|
|
|
+ <el-option v-for="opt in itemOptions[item._key] || []" :key="opt.value"
|
|
|
|
|
+ :label="opt.label" :value="opt.value"
|
|
|
|
|
+ :disabled="isOptionDisabled(opt.value, index)" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-input v-model="item.jumpUrl" placeholder="请输入跳转链接">
|
|
|
|
|
+ <template #prepend>链接</template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ <div v-if="item.showCateId" class="text-xs text-gray-400 mt-1">
|
|
|
|
|
+ 已绑定书单ID: {{ item.showCateId }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Delete Button -->
|
|
|
|
|
+ <el-button type="danger" circle size="small"
|
|
|
|
|
+ class="opacity-0 group-hover:opacity-100 transition-opacity absolute -top-2 -right-2"
|
|
|
|
|
+ @click="handleRemove(index)">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Delete />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </draggable>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Add Button -->
|
|
|
|
|
+ <el-button v-if="items.length < limit" class="w-full border-dashed mt-4" @click="handleAdd">
|
|
|
|
|
+ <el-icon class="mr-1">
|
|
|
|
|
+ <Plus />
|
|
|
|
|
+ </el-icon> 添加
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<template #footer>
|
|
<template #footer>
|
|
@@ -34,11 +62,12 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
- import { computed, ref, watch } from 'vue';
|
|
|
|
|
- import { Plus, Delete } from '@element-plus/icons-vue';
|
|
|
|
|
|
|
+ import { computed, ref } from 'vue';
|
|
|
|
|
+ import { Plus, Delete, Rank } from '@element-plus/icons-vue';
|
|
|
import ImageUpload from '@/components/ImageUpload/index.vue';
|
|
import ImageUpload from '@/components/ImageUpload/index.vue';
|
|
|
import request from '@/utils/request';
|
|
import request from '@/utils/request';
|
|
|
import { EleMessage } from 'ele-admin-plus/es';
|
|
import { EleMessage } from 'ele-admin-plus/es';
|
|
|
|
|
+ import draggable from 'vuedraggable';
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
modelValue: {
|
|
modelValue: {
|
|
@@ -67,27 +96,23 @@
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
- const selectDialogVisible = ref(false);
|
|
|
|
|
- const currentSelectIndex = ref(-1);
|
|
|
|
|
-
|
|
|
|
|
- const options = ref(Array(props.limit).fill([]));
|
|
|
|
|
|
|
+ const items = ref([]);
|
|
|
|
|
+ const itemOptions = ref({});
|
|
|
const defaultOptions = ref([]);
|
|
const defaultOptions = ref([]);
|
|
|
|
|
|
|
|
- const items = ref(Array(props.limit).fill(null).map(() => ({
|
|
|
|
|
|
|
+ const createItem = () => ({
|
|
|
|
|
+ _key: Date.now() + Math.random(),
|
|
|
title: '',
|
|
title: '',
|
|
|
imgUrl: '',
|
|
imgUrl: '',
|
|
|
jumpUrl: '',
|
|
jumpUrl: '',
|
|
|
showCateId: undefined,
|
|
showCateId: undefined,
|
|
|
loading: false
|
|
loading: false
|
|
|
- })));
|
|
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
// 计算选项是否应被禁用
|
|
// 计算选项是否应被禁用
|
|
|
const isOptionDisabled = (value, currentIndex) => {
|
|
const isOptionDisabled = (value, currentIndex) => {
|
|
|
- // 检查该值是否已被其他项选中
|
|
|
|
|
return items.value.some((item, index) => {
|
|
return items.value.some((item, index) => {
|
|
|
- // 跳过当前正在编辑的项
|
|
|
|
|
if (index === currentIndex) return false;
|
|
if (index === currentIndex) return false;
|
|
|
- // 如果其他项选中了该值,则禁用
|
|
|
|
|
return item.showCateId === value;
|
|
return item.showCateId === value;
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
@@ -116,8 +141,11 @@
|
|
|
|
|
|
|
|
// 远程搜索书单
|
|
// 远程搜索书单
|
|
|
const remoteMethod = async (query, index) => {
|
|
const remoteMethod = async (query, index) => {
|
|
|
|
|
+ const item = items.value[index];
|
|
|
|
|
+ const key = item._key;
|
|
|
|
|
+
|
|
|
if (query) {
|
|
if (query) {
|
|
|
- items.value[index].loading = true;
|
|
|
|
|
|
|
+ item.loading = true;
|
|
|
try {
|
|
try {
|
|
|
const res = await request.get('/book/showIndex/list', {
|
|
const res = await request.get('/book/showIndex/list', {
|
|
|
params: {
|
|
params: {
|
|
@@ -128,33 +156,31 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
const list = res.data?.rows || res.data || [];
|
|
const list = res.data?.rows || res.data || [];
|
|
|
- options.value[index] = list.map(item => ({
|
|
|
|
|
|
|
+ itemOptions.value[key] = list.map(item => ({
|
|
|
value: item.id,
|
|
value: item.id,
|
|
|
label: item.showName,
|
|
label: item.showName,
|
|
|
raw: item
|
|
raw: item
|
|
|
}));
|
|
}));
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error(e);
|
|
console.error(e);
|
|
|
- options.value[index] = [];
|
|
|
|
|
|
|
+ itemOptions.value[key] = [];
|
|
|
} finally {
|
|
} finally {
|
|
|
- items.value[index].loading = false;
|
|
|
|
|
|
|
+ item.loading = false;
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // 恢复默认选项,但保留当前选中的项(如果不在默认列表中)
|
|
|
|
|
- const currentSelectedId = items.value[index].showCateId;
|
|
|
|
|
|
|
+ // 恢复默认选项,但保留当前选中的项
|
|
|
|
|
+ const currentSelectedId = item.showCateId;
|
|
|
let currentOption = null;
|
|
let currentOption = null;
|
|
|
|
|
|
|
|
- // 尝试在当前选项或默认选项中找到当前选中的项
|
|
|
|
|
if (currentSelectedId) {
|
|
if (currentSelectedId) {
|
|
|
- currentOption = options.value[index].find(opt => opt.value === currentSelectedId) ||
|
|
|
|
|
|
|
+ currentOption = (itemOptions.value[key] || []).find(opt => opt.value === currentSelectedId) ||
|
|
|
defaultOptions.value.find(opt => opt.value === currentSelectedId);
|
|
defaultOptions.value.find(opt => opt.value === currentSelectedId);
|
|
|
|
|
|
|
|
- // 如果都没找到,检查是否有手动注入的数据(回显时)
|
|
|
|
|
- if (!currentOption && items.value[index].title) {
|
|
|
|
|
|
|
+ if (!currentOption && item.title) {
|
|
|
currentOption = {
|
|
currentOption = {
|
|
|
value: currentSelectedId,
|
|
value: currentSelectedId,
|
|
|
- label: items.value[index].title,
|
|
|
|
|
- raw: { imgUrl: items.value[index].imgUrl, showName: items.value[index].title }
|
|
|
|
|
|
|
+ label: item.title,
|
|
|
|
|
+ raw: { imgUrl: item.imgUrl, showName: item.title }
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -163,59 +189,80 @@
|
|
|
if (currentOption && !merged.some(opt => opt.value === currentOption.value)) {
|
|
if (currentOption && !merged.some(opt => opt.value === currentOption.value)) {
|
|
|
merged.unshift(currentOption);
|
|
merged.unshift(currentOption);
|
|
|
}
|
|
}
|
|
|
- options.value[index] = merged;
|
|
|
|
|
|
|
+ itemOptions.value[key] = merged;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 处理下拉框选择变化
|
|
// 处理下拉框选择变化
|
|
|
const handleSelectChange = (val, index) => {
|
|
const handleSelectChange = (val, index) => {
|
|
|
|
|
+ const item = items.value[index];
|
|
|
|
|
+ const key = item._key;
|
|
|
|
|
+
|
|
|
if (!val) {
|
|
if (!val) {
|
|
|
- // 清空选择
|
|
|
|
|
- items.value[index].showCateId = undefined;
|
|
|
|
|
|
|
+ item.showCateId = undefined;
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- const selectedOption = options.value[index].find(opt => opt.value === val);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const opts = itemOptions.value[key] || defaultOptions.value;
|
|
|
|
|
+ const selectedOption = opts.find(opt => opt.value === val);
|
|
|
|
|
+
|
|
|
if (selectedOption) {
|
|
if (selectedOption) {
|
|
|
const raw = selectedOption.raw;
|
|
const raw = selectedOption.raw;
|
|
|
- items.value[index].imgUrl = raw.imgUrl;
|
|
|
|
|
- items.value[index].title = raw.showName;
|
|
|
|
|
- items.value[index].showCateId = raw.id;
|
|
|
|
|
- items.value[index].jumpUrl = `/pages-sell/pages/recommend?id=${raw.id}`;
|
|
|
|
|
|
|
+ item.imgUrl = raw.imgUrl;
|
|
|
|
|
+ item.title = raw.showName;
|
|
|
|
|
+ item.showCateId = raw.id;
|
|
|
|
|
+ item.jumpUrl = `/pages-sell/pages/recommend?id=${raw.id}`;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleAdd = () => {
|
|
|
|
|
+ if (items.value.length < props.limit) {
|
|
|
|
|
+ const newItem = createItem();
|
|
|
|
|
+ items.value.push(newItem);
|
|
|
|
|
+ itemOptions.value[newItem._key] = [...defaultOptions.value];
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleRemove = (index) => {
|
|
|
|
|
+ const item = items.value[index];
|
|
|
|
|
+ delete itemOptions.value[item._key];
|
|
|
|
|
+ items.value.splice(index, 1);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// 打开弹窗初始化
|
|
// 打开弹窗初始化
|
|
|
const handleOpen = async () => {
|
|
const handleOpen = async () => {
|
|
|
- // 先获取默认选项
|
|
|
|
|
await fetchDefaultOptions();
|
|
await fetchDefaultOptions();
|
|
|
|
|
+ itemOptions.value = {};
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const res = await request.get('/book/showIndex/getInfoByPosition', {
|
|
const res = await request.get('/book/showIndex/getInfoByPosition', {
|
|
|
params: { position: props.position }
|
|
params: { position: props.position }
|
|
|
});
|
|
});
|
|
|
const list = res.data?.data || [];
|
|
const list = res.data?.data || [];
|
|
|
- console.log('list', list);
|
|
|
|
|
- // 填充数据,保持结构一致
|
|
|
|
|
- items.value = Array(props.limit).fill(null).map((_, i) => {
|
|
|
|
|
- const remote = list[i] || {};
|
|
|
|
|
- return {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (list.length > 0) {
|
|
|
|
|
+ items.value = list.map(remote => ({
|
|
|
|
|
+ _key: Date.now() + Math.random(),
|
|
|
title: remote.title || '',
|
|
title: remote.title || '',
|
|
|
imgUrl: remote.imgUrl || '',
|
|
imgUrl: remote.imgUrl || '',
|
|
|
jumpUrl: remote.jumpUrl || '',
|
|
jumpUrl: remote.jumpUrl || '',
|
|
|
showCateId: remote.showCateId,
|
|
showCateId: remote.showCateId,
|
|
|
loading: false
|
|
loading: false
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ }));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ items.value = [];
|
|
|
|
|
+ // 如果需要默认显示一个空项,可以在这里添加
|
|
|
|
|
+ if (items.value.length === 0) {
|
|
|
|
|
+ items.value.push(createItem());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 初始化每个项目的选项(默认选项 + 当前选中项)
|
|
|
|
|
- items.value.forEach((item, index) => {
|
|
|
|
|
|
|
+ // 初始化每个项目的选项
|
|
|
|
|
+ items.value.forEach(item => {
|
|
|
const merged = [...defaultOptions.value];
|
|
const merged = [...defaultOptions.value];
|
|
|
-
|
|
|
|
|
if (item.showCateId) {
|
|
if (item.showCateId) {
|
|
|
- // 检查当前选中项是否已在默认选项中
|
|
|
|
|
const exists = merged.some(opt => opt.value === item.showCateId);
|
|
const exists = merged.some(opt => opt.value === item.showCateId);
|
|
|
if (!exists && item.title) {
|
|
if (!exists && item.title) {
|
|
|
- // 如果不在,手动注入当前项以确保回显正确
|
|
|
|
|
merged.unshift({
|
|
merged.unshift({
|
|
|
value: item.showCateId,
|
|
value: item.showCateId,
|
|
|
label: item.title,
|
|
label: item.title,
|
|
@@ -223,19 +270,12 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- options.value[index] = merged;
|
|
|
|
|
|
|
+ itemOptions.value[item._key] = merged;
|
|
|
});
|
|
});
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error(e);
|
|
console.error(e);
|
|
|
- // 出错或为空时重置
|
|
|
|
|
- items.value = Array(props.limit).fill(null).map(() => ({
|
|
|
|
|
- title: '',
|
|
|
|
|
- imgUrl: '',
|
|
|
|
|
- jumpUrl: '',
|
|
|
|
|
- showCateId: undefined,
|
|
|
|
|
- loading: false
|
|
|
|
|
- }));
|
|
|
|
|
- options.value = Array(props.limit).fill(defaultOptions.value);
|
|
|
|
|
|
|
+ items.value = [createItem()];
|
|
|
|
|
+ itemOptions.value[items.value[0]._key] = [...defaultOptions.value];
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -250,7 +290,6 @@
|
|
|
jumpUrl: item.jumpUrl,
|
|
jumpUrl: item.jumpUrl,
|
|
|
showCateId: item.showCateId || 0,
|
|
showCateId: item.showCateId || 0,
|
|
|
orderNum: index,
|
|
orderNum: index,
|
|
|
- // 添加标题,以便后端支持时保存,或用于前端回显
|
|
|
|
|
title: item.title
|
|
title: item.title
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
@@ -275,5 +314,7 @@
|
|
|
.diamond-edit {
|
|
.diamond-edit {
|
|
|
max-height: 60vh;
|
|
max-height: 60vh;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ overflow-x: hidden;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|