修复总结后的问题,详见企业微信文档

This commit is contained in:
gyq
2026-05-07 14:34:03 +08:00
parent ffad9432c5
commit ea5a4c14e9
41 changed files with 2678 additions and 1120 deletions

View File

@@ -0,0 +1,417 @@
<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>