Prechádzať zdrojové kódy

feat(salesOps): 为金刚位编辑组件添加拖拽排序与动态增删功能

ylong 2 dní pred
rodič
commit
8c9adbad76

+ 116 - 75
src/views/salesOps/decoration/components/DiamondEdit.vue

@@ -1,27 +1,55 @@
 <template>
     <el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false" @open="handleOpen">
         <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>
+                </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>
 
         <template #footer>
@@ -34,11 +62,12 @@
 </template>
 
 <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 request from '@/utils/request';
     import { EleMessage } from 'ele-admin-plus/es';
+    import draggable from 'vuedraggable';
 
     const props = defineProps({
         modelValue: {
@@ -67,27 +96,23 @@
     });
 
     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 items = ref(Array(props.limit).fill(null).map(() => ({
+    const createItem = () => ({
+        _key: Date.now() + Math.random(),
         title: '',
         imgUrl: '',
         jumpUrl: '',
         showCateId: undefined,
         loading: false
-    })));
+    });
 
     // 计算选项是否应被禁用
     const isOptionDisabled = (value, currentIndex) => {
-        // 检查该值是否已被其他项选中
         return items.value.some((item, index) => {
-            // 跳过当前正在编辑的项
             if (index === currentIndex) return false;
-            // 如果其他项选中了该值,则禁用
             return item.showCateId === value;
         });
     };
@@ -116,8 +141,11 @@
 
     // 远程搜索书单
     const remoteMethod = async (query, index) => {
+        const item = items.value[index];
+        const key = item._key;
+
         if (query) {
-            items.value[index].loading = true;
+            item.loading = true;
             try {
                 const res = await request.get('/book/showIndex/list', {
                     params: {
@@ -128,33 +156,31 @@
                     }
                 });
                 const list = res.data?.rows || res.data || [];
-                options.value[index] = list.map(item => ({
+                itemOptions.value[key] = list.map(item => ({
                     value: item.id,
                     label: item.showName,
                     raw: item
                 }));
             } catch (e) {
                 console.error(e);
-                options.value[index] = [];
+                itemOptions.value[key] = [];
             } finally {
-                items.value[index].loading = false;
+                item.loading = false;
             }
         } else {
-            // 恢复默认选项,但保留当前选中的项(如果不在默认列表中)
-            const currentSelectedId = items.value[index].showCateId;
+            // 恢复默认选项,但保留当前选中的项
+            const currentSelectedId = item.showCateId;
             let currentOption = null;
 
-            // 尝试在当前选项或默认选项中找到当前选中的项
             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);
 
-                // 如果都没找到,检查是否有手动注入的数据(回显时)
-                if (!currentOption && items.value[index].title) {
+                if (!currentOption && item.title) {
                     currentOption = {
                         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)) {
                 merged.unshift(currentOption);
             }
-            options.value[index] = merged;
+            itemOptions.value[key] = merged;
         }
     };
 
     // 处理下拉框选择变化
     const handleSelectChange = (val, index) => {
+        const item = items.value[index];
+        const key = item._key;
+
         if (!val) {
-            // 清空选择
-            items.value[index].showCateId = undefined;
+            item.showCateId = undefined;
             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) {
             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 () => {
-        // 先获取默认选项
         await fetchDefaultOptions();
+        itemOptions.value = {};
 
         try {
             const res = await request.get('/book/showIndex/getInfoByPosition', {
                 params: { position: props.position }
             });
             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 || '',
                     imgUrl: remote.imgUrl || '',
                     jumpUrl: remote.jumpUrl || '',
                     showCateId: remote.showCateId,
                     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];
-
                 if (item.showCateId) {
-                    // 检查当前选中项是否已在默认选项中
                     const exists = merged.some(opt => opt.value === item.showCateId);
                     if (!exists && item.title) {
-                        // 如果不在,手动注入当前项以确保回显正确
                         merged.unshift({
                             value: item.showCateId,
                             label: item.title,
@@ -223,19 +270,12 @@
                         });
                     }
                 }
-                options.value[index] = merged;
+                itemOptions.value[item._key] = merged;
             });
         } catch (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,
                     showCateId: item.showCateId || 0,
                     orderNum: index,
-                    // 添加标题,以便后端支持时保存,或用于前端回显
                     title: item.title
                 }));
 
@@ -275,5 +314,7 @@
     .diamond-edit {
         max-height: 60vh;
         overflow-y: auto;
+        padding: 10px;
+        overflow-x: hidden;
     }
 </style>