Files
cashier-web/src/views/product/components/addSkuModal.vue

417 lines
10 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>
<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>