417 lines
10 KiB
Vue
417 lines
10 KiB
Vue
<template>
|
||
<el-dialog :title="form.id ? '编辑模板' : '新增模板'" width="900px" v-model="visible" @close="onClose">
|
||
<div class="form_wrap">
|
||
<div class="form_left">
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100" label-position="right">
|
||
<el-form-item label="模板名称" prop="name">
|
||
<el-input v-model="form.name" :maxlength="20" placeholder="如:主食、酒水、辣度、大小等"></el-input>
|
||
</el-form-item>
|
||
|
||
<transition-group name="slide">
|
||
<div class="sortable-parent" v-for="(item, index) in form.children" :key="item._id"
|
||
:data-parent-index="index"> <!-- 修复Vue3绑定语法 -->
|
||
<el-form-item label="规格名称">
|
||
<div class="center">
|
||
<el-input v-model="item.name" placeholder="如:口味、忌口、温度、分量等"></el-input>
|
||
<el-icon v-if="index > 0" size="20" @click="form.children.splice(index, 1)">
|
||
<Delete />
|
||
</el-icon>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<transition-group name="fade" tag="div">
|
||
<el-form-item label="规格值" v-for="(val, i) in item.children" :key="val._id" :data-child-index="i">
|
||
<!-- 修复Vue3绑定语法 -->
|
||
<div class="center">
|
||
<div style="flex:1;">
|
||
<el-input v-model="val.name" placeholder="如:五香、微辣、大份、小份等"></el-input>
|
||
</div>
|
||
<el-icon v-if="i > 0" size="20" @click="form.children[index].children.splice(i, 1)">
|
||
<Delete />
|
||
</el-icon>
|
||
</div>
|
||
</el-form-item>
|
||
</transition-group>
|
||
|
||
<el-form-item style="margin-top:-10px;">
|
||
<el-button size="small" @click="addSpecValue(index)">添加规格值</el-button>
|
||
</el-form-item>
|
||
</div>
|
||
</transition-group>
|
||
|
||
<el-form-item label-width="0">
|
||
<div class="flex_end">
|
||
<el-button type="primary" @click="addSpec">添加规格</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="form_right">
|
||
<div class="preview_wrap">
|
||
<div class="title_wrap">
|
||
<el-text size="large">效果预览</el-text>
|
||
<el-text size="small">可拖动排序</el-text>
|
||
</div>
|
||
|
||
<div class="row_wrap" id="parent-sort">
|
||
<!-- 修复index未定义 + 绑定语法错误 -->
|
||
<div class="row" v-for="(item, index) in form.children" :key="item._id" :data-index="index">
|
||
<div class="row_top">
|
||
<el-text size="large">{{ item.name || '请输入规格名称' }}</el-text>
|
||
<el-icon size="20" class="drag-handle">
|
||
<Rank />
|
||
</el-icon>
|
||
</div>
|
||
|
||
<div class="row_content" v-if="item.children.length" :id="`child-sort-${item._id}`">
|
||
<div class="tag" v-for="val in item.children" :key="val._id">
|
||
<el-text>{{ val.name || '请输入规格值' }}</el-text>
|
||
</div>
|
||
</div>
|
||
<div class="tag empty-tag" v-else>
|
||
<el-text>请添加规格值</el-text>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="visible = false">取 消</el-button>
|
||
<el-button type="primary" @click="handleOk" :loading="loading">确 定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import _ from 'lodash'
|
||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Delete, Rank } from '@element-plus/icons-vue'
|
||
import Sortable from 'sortablejs'
|
||
import UserAPI from "@/api/product/specificationsconfig";
|
||
|
||
const generateId = () => {
|
||
return Date.now() + '_' + Math.floor(Math.random() * 10000)
|
||
}
|
||
|
||
const childrenObj = ref({
|
||
_id: generateId(),
|
||
name: '',
|
||
children: [{ _id: generateId(), name: '' }]
|
||
})
|
||
|
||
const formObj = {
|
||
_id: '',
|
||
name: '',
|
||
children: [_.cloneDeep(childrenObj.value)]
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const formRef = ref(null)
|
||
const form = ref(formObj)
|
||
|
||
const visible = ref(false)
|
||
|
||
const rules = ref({
|
||
name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
|
||
})
|
||
|
||
function onClose() {
|
||
form.value = _.cloneDeep(formObj)
|
||
// if (parentSortable) parentSortable.destroy()
|
||
// childSortableMap.forEach(s => s.destroy())
|
||
// childSortableMap.clear()
|
||
|
||
// setTimeout(() => {
|
||
// }, 50);
|
||
visible.value = false
|
||
}
|
||
|
||
function addSpec() {
|
||
form.value.children.push({
|
||
_id: generateId(),
|
||
name: '',
|
||
children: [{ _id: generateId(), name: '' }]
|
||
})
|
||
}
|
||
|
||
function addSpecValue(parentIndex) {
|
||
form.value.children[parentIndex].children.push({
|
||
_id: generateId(),
|
||
name: ''
|
||
})
|
||
}
|
||
|
||
const emit = defineEmits(['success'])
|
||
function handleOk() {
|
||
formRef.value.validate(async (valid) => {
|
||
try {
|
||
if (valid) {
|
||
let flag = true
|
||
let pIndex = 0
|
||
let index = 0
|
||
form.value.children.forEach((item, index) => {
|
||
if (item.name == '') {
|
||
flag = false
|
||
pIndex = index
|
||
return
|
||
}
|
||
item.children.forEach((val, i) => {
|
||
if (val.name == '') {
|
||
flag = false
|
||
pIndex = index
|
||
index = i
|
||
return
|
||
}
|
||
})
|
||
})
|
||
|
||
if (!flag) {
|
||
ElMessage.error('请补全规格信息')
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
console.log('最终提交数据:', form.value)
|
||
|
||
await UserAPI.quickAdd(form.value)
|
||
ElMessage.success(form.value.id ? '编辑成功' : '添加成功')
|
||
visible.value = false
|
||
loading.value = false
|
||
|
||
emit('success')
|
||
}
|
||
} catch (error) {
|
||
console.log(error);
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
function open(obj) {
|
||
visible.value = true
|
||
if (obj && obj.id) {
|
||
obj.children.forEach(item => {
|
||
item._id = generateId()
|
||
item.children.forEach(val => {
|
||
val._id = generateId()
|
||
})
|
||
})
|
||
form.value = _.cloneDeep(obj)
|
||
console.log(form.value);
|
||
}
|
||
}
|
||
|
||
defineExpose({
|
||
open
|
||
})
|
||
|
||
let parentSortable = null
|
||
const childSortableMap = new Map()
|
||
|
||
onMounted(() => {
|
||
nextTick(() => {
|
||
initParentSort()
|
||
initAllChildSort()
|
||
})
|
||
})
|
||
|
||
watch(() => form.value.children, () => {
|
||
nextTick(() => {
|
||
initParentSort()
|
||
initAllChildSort()
|
||
})
|
||
}, { deep: true })
|
||
|
||
function initParentSort() {
|
||
const el = document.getElementById('parent-sort')
|
||
if (!el) return
|
||
if (parentSortable) parentSortable.destroy()
|
||
|
||
parentSortable = Sortable.create(el, {
|
||
handle: '.drag-handle', // 仅拖拽手柄生效
|
||
animation: 200,
|
||
ghostClass: 'sort-ghost', // 仅保留占位提示
|
||
forceFallback: true,
|
||
fallbackOnBody: false, // 禁用body上的克隆预览
|
||
fallbackClone: false, // 禁用拖拽克隆元素(核心:移除跟随预览)
|
||
// 移除dragClass,禁用拖拽预览样式
|
||
onEnd(evt) {
|
||
const { oldIndex, newIndex } = evt
|
||
if (oldIndex === newIndex) return
|
||
const moved = form.value.children.splice(oldIndex, 1)[0]
|
||
form.value.children.splice(newIndex, 0, moved)
|
||
nextTick(initAllChildSort)
|
||
}
|
||
})
|
||
}
|
||
|
||
function initAllChildSort() {
|
||
childSortableMap.forEach(s => s.destroy())
|
||
childSortableMap.clear()
|
||
|
||
form.value.children.forEach(item => {
|
||
const id = `child-sort-${item._id}`
|
||
const el = document.getElementById(id)
|
||
if (!el) return
|
||
|
||
const s = Sortable.create(el, {
|
||
animation: 200,
|
||
ghostClass: 'sort-ghost', // 仅保留占位提示
|
||
direction: 'horizontal',
|
||
forceFallback: true,
|
||
fallbackOnBody: false, // 禁用body上的克隆预览
|
||
fallbackClone: false, // 禁用拖拽克隆元素(核心)
|
||
draggable: '.tag', // 仅tag可拖拽
|
||
// 移除dragClass,禁用拖拽预览样式
|
||
onEnd(evt) {
|
||
const { oldIndex, newIndex } = evt
|
||
if (oldIndex === newIndex) return
|
||
const list = item.children
|
||
const moved = list.splice(oldIndex, 1)[0]
|
||
list.splice(newIndex, 0, moved)
|
||
}
|
||
})
|
||
childSortableMap.set(id, s)
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.flex_end {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.sortable-parent {
|
||
border: 1px solid #ececec;
|
||
padding: 14px;
|
||
border-radius: 4px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.form_wrap {
|
||
display: flex;
|
||
gap: 24px;
|
||
|
||
.form_left {
|
||
flex: 1;
|
||
height: 60vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.form_right {
|
||
flex: 1;
|
||
}
|
||
|
||
.preview_wrap {
|
||
border: 1px solid #ececec;
|
||
border-radius: 4px;
|
||
padding: 14px;
|
||
|
||
.title_wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.row_wrap {
|
||
padding-top: 14px;
|
||
|
||
.row {
|
||
margin-bottom: 16px;
|
||
position: relative;
|
||
|
||
.row_top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
cursor: default;
|
||
padding-bottom: 10px;
|
||
|
||
.drag-handle {
|
||
cursor: grab;
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
}
|
||
|
||
.row_content {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
min-height: 36px;
|
||
}
|
||
|
||
.tag {
|
||
padding: 4px 20px;
|
||
border-radius: 4px;
|
||
background: #e6e6e6;
|
||
cursor: grab;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
|
||
.empty-tag {
|
||
background: #f5f5f5;
|
||
color: #999;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.center {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
// 仅保留占位提示样式(无跟随预览)
|
||
:deep(.sort-ghost) {
|
||
opacity: 0.4;
|
||
background: #f5f5f5 !important;
|
||
border: 1px dashed #409eff !important;
|
||
}
|
||
|
||
// 移除所有拖拽预览相关样式
|
||
:deep(.parent-dragging),
|
||
:deep(.child-dragging) {
|
||
display: none !important;
|
||
}
|
||
|
||
.slide-move-active,
|
||
.fade-move-active {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.slide-enter-from,
|
||
.slide-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(-20px);
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
}
|
||
</style> |