VueDraggable 子元素input选中文字会出现拖动

博客讲述了在Vue项目中遇到的一个问题:当尝试选中input内的文字时,由于事件冒泡导致触发了draggable组件的拖动效果。作者通过分析发现,点击input文字边缘会触发draggable的pointerdown事件。为了解决这个问题,作者在input元素上添加了@pointerdown.stop.native修饰符来阻止事件冒泡,从而成功阻止了不必要的拖动行为,确保了input文字的正常选中功能。
<el-form-item label="Label" prop="prop">
	<draggable v-model="form.label" @start="drag=true" @end="drag=false">
		<div v-for="(ele, idx) in form.label" :key="index">
			<el-input type="textarea" />
			<el-button>del</el-button>
		</div>
	<draggable>
</el-form-item>

这段代码导致的问题是,在希望选中input中文字的时候,若点击了文字的开头或结尾,会导致出现拖动效果,然而此时希望的是选中input中文字内容。

原因:在鼠标点击input时,事件冒泡,触发了draggable div的某个事件(并不一定是click),造成了拖动的效果
解决:在input元素中阻止该事件的冒泡。
步骤:1.找到触发的具体事件,如下图
在这里插入图片描述
在这里插入图片描述

2.在textarea中阻止事件冒泡,使用vue提供的事件修饰符进行处理。元素是el-input,事件是pointerdown,修饰符是stop和native组合(stop和native不赘述)。代码修改如下

<el-form-item label="Label" prop="prop">
	<draggable v-model="form.label" @start="drag=true" @end="drag=false">
		<div v-for="(ele, idx) in form.label" :key="index">
			<el-input @pointerdown.stop.native type="textarea" />
			<el-button>del</el-button>
		</div>
	<draggable>
</el-form-item>

问题解决。

<!-- 工艺模板 --> <template> <el-container v-loading="loading"> <!-- 工具栏 --> <el-header> <!-- 左侧工具栏 --> <!-- <div class="left-panel"> <el-button type="primary" icon="el-icon-plus" @click="edit()">新增</el-button> </div> --> <!-- 右侧工具栏 --> <div class="right-panel"> <div class="right-panel-search"> <el-input v-model="procedureData.name" placeholder="名称" clearable /> <el-button icon="el-icon-refresh" @click="refreshProcedure">查看所有</el-button> <el-button type="primary" icon="el-icon-search" @click="getProcedure(0)">搜索</el-button> <el-button type="primary" icon="el-icon-plus" @click="editProcedure()">新增</el-button> </div> </div> </el-header> <!-- 列表 --> <el-main class="nopadding"> <scTable :data="procedureData.data" style="width: 100%" row-key="id" :getList="getProcedure" hidePagination :header-cell-style="{ background: &#39;#F1F3F0&#39;, color: &#39;#7B7571&#39; }"> <el-table-column label="ID" prop="id" width="50"></el-table-column> <el-table-column label="编号" prop="rule_number"></el-table-column> <el-table-column label="工艺名称" prop="name"></el-table-column> <el-table-column label="操作人" prop="user_name"></el-table-column> <el-table-column label="创建时间" prop="create_time"></el-table-column> <el-table-column label="操作" fixed="right" width="150"> <template #default="scope"> <el-button plain type="primary" size="small" @click="editProcedure(scope.row)">编辑</el-button> <el-button plain type="primary" size="small" @click="delProcedure(scope.row)">删除</el-button> </template> </el-table-column> <template #pagination> <el-pagination layout="prev, pager, next, total, sizes" :current-page="procedureData.current_page" :page-size="procedureData.per_page" :total="procedureData.total" :page-sizes="[20, 50, 100]" @size-change=" (num) => { procedureData.per_page = num; getProcedure(1); } " @current-change="getProcedure" /> </template> </scTable> </el-main> <!-- 工艺工序新增编辑模板 --> <el-dialog v-model="ditVisibleGy" :close-on-click-modal="false" width="80%" :fullscreen="isFullscreen" class="dialogGxgy"> <!-- 自定义标题栏 --> <template #title> <div class="dialogtitle"> <h4 style="font-size: 17px;">{{ procedureForm.id ? &#39;编辑工艺模板&#39; : &#39;新增工艺模板&#39; }}</h4> <div class="dialog-icons"> <!-- 全屏图标 --> <el-icon @click="toggleFullscreen" class="fullscreen-icon"> <component :is="isFullscreen ? ZoomOut : FullScreen" /> </el-icon> </div> </div> </template> <el-header> <div class="gybs"> <div class="gyheader"> <h4>模板名称:</h4> <el-input v-model="procedureForm.name" placeholder="请输入模板名称" style="width: 200px" /> </div> <!-- <el-upload class="sc-file-select__upload" :action="action" multiple :show-file-list="false" :accept="accept" :before-upload="uploadBefore" :on-success="uploadSuccess" :on-error="uploadError" :headers="uploadHeaders" :data="uploadData"> <el-button type="primary" icon="el-icon-upload">上传图纸</el-button> </el-upload> --> <!-- 图片列表,横向滚动 --> </div> </el-header> <el-container v-loading="gymbLoading"> <el-aside width="250px" height="400px"> <el-card class="card_gy"> <!-- 拖拽排序 --> <div class="tzpx"> <fcDraggable v-model="itemsgy" :sort="true" handle=".icon-drag" itemKey="gongxumingcheng" direction="vertical" :animation="300" @end="onDragEnd"> <template #item="{ element, index }"> <div class="slx"> <div class="sortable-item" @click="selectItem(element)" :class="{ &#39;selected&#39;: selectedItem === element }"> <div class="item_left"> <i class="fc-icon icon-drag"></i> </div> <div class="item_content"> {{ element.gongxumingcheng }} </div> <div class="item_right"> <i class="fc-icon icon-delete" @click.stop="removeField(index)"></i> </div> </div> </div> </template> </fcDraggable> <el-empty description="暂无工序" :image-size="120" v-if="itemsgy.length === 0" /> </div> <el-button-group class="btgp"> <el-button type="primary" @click="addProcedure">添加</el-button> <el-button type="primary" @click="changeGx">选择工序</el-button> </el-button-group> </el-card> </el-aside> <el-container class="" v-loading="bomLoading" v-if="itemsgy.length > 0"> <el-header> <H3 class="ht">工序参数</H3> </el-header> <div class="formbody"> <div class="gyform" v-if="isFormFilled"> <form-create v-model:api="api" :rule="formRule" :option="formCreateOptions" /> </div> <el-empty description="请先选择工序" v-show="!isFormFilled" /> <div class="gycs" v-if="itemsgy.length > 1"> <div class="dowt"> <H3>工艺路线图</H3> </div> <div class="route"> <el-steps :active="itemsgy.length" :space="150" align-center finish-status="primary" process-status="primary"> <el-step v-for="(item, index) in itemsgy" :key="index" :title="item.gongxumingcheng" /> </el-steps> </div> </div> </div> </el-container> <div v-else style="width: 100%;"> <el-empty description="请先选择工序模板" /> </div> <el-aside width="200px" class="lf_aside"> <div class="image-preview-list"> <el-scrollbar height="400px"> <el-upload class="uploadTz" :action="action" multiple :show-file-list="false" :on-preview="handlePictureCardPreview" :before-upload="uploadBefore" :on-success="uploadSuccess" :on-error="uploadError" :headers="uploadHeaders" :data="uploadData" > <el-button type="primary">上传图纸</el-button> </el-upload> <div class="upbox"> <div class="wdg" v-for="(item, index) in fileList" :key="index"> <div class="wg_lf" @click="handlePictureCardPreview(item)"> <el-icon size="18"> <DocumentRemove /> </el-icon> <el-tooltip class="box-item" effect="dark" :content="item.original_name" placement="top"> <div class="weds">{{ item.original_name }}</div> </el-tooltip> </div> <div> <el-button type="danger" :icon="Delete" circle size="small" @click="delImg(index)"/> </div> </div> </div> <el-dialog v-model="dialogVisibleImg"> <img w-full :src="dialogImageUrl" alt="Preview Image" /> </el-dialog> </el-scrollbar> </div> </el-aside> </el-container> <template #footer> <span class="dialog-footer"> <el-button @click="ditVisibleGy = false">关闭</el-button> <el-button type="primary" @click="submitProcesses" :loading="gyloading"> 保存工艺模板 </el-button> </span> </template> </el-dialog> <!-- 选择工序弹窗 --> <el-dialog v-model="dialogVisiblexzgx" title="选择工序" width="60%"> <el-container> <el-aside width="200px"> <el-tree ref="dic" class="menu" node-key="id" :data="gxtypeList" :props="{ label: &#39;name&#39;, }" :highlight-current="true" :expand-on-click-node="false" :filter-node-method="dicFilterNode" @node-click="selectgxClass"> <template #default="{ node, data }"> <span class="custom-tree-node"> <span class="label">{{ node.label }}</span> <span class="code">{{ data.code }}</span> </span> </template> </el-tree> </el-aside> <el-main v-loading="tableloading"> <el-header v-if="processesList.data.length > 0"> <div class="right-panel"> <el-input v-model="processesList.name" placeholder="标题" class="titlese" /> <el-button icon="el-icon-refresh" @click="refresprocessesList">查看所有</el-button> <el-button type="primary" icon="el-icon-search" @click="getTemplate(1)"></el-button> </div> </el-header> <el-main> <scTable :data="processesList.data" ref="tableRef" style="width: 100%" row-key="id" :getList="getitem" @row-click="handleRowClick" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="50"></el-table-column> <el-table-column prop="id" label="ID" width="50" /> <el-table-column prop="gongxumingcheng" label="工序名称" fixed="right" /> <el-table-column prop="user_name" label="创建人" fixed="right" /> <el-table-column prop="create_time" label="创建时间" fixed="right" /> <template #pagination> <el-pagination layout="prev, pager, next, total, sizes" :current-page="processesList.current_page" :page-size="processesList.per_page" :total="processesList.total" :page-sizes="[20, 50, 100]" @size-change=" (num) => { processesList.per_page = num; getTemplate(1); } " @current-change="getTemplate" /> </template> </scTable> </el-main> </el-main> </el-container> <template #footer> <div class="dialog-footer"> <el-button @click="dialogVisiblexzgx = false">取消</el-button> <el-button type="primary" @click="selectProcess" :loading="selectProcessLoading"> 确定 </el-button> </div> </template> </el-dialog> <!-- 新增工序 公共页面弹窗新增 --> <el-dialog v-model="ditVisibleProcedure" :close-on-click-modal="false" width="70%" title="工序新增"> <form-create v-model:api="api" :rule="formRuleAdd" :option="formCreateOptions" /> </el-dialog> </el-container> </template> <script setup> import { ref, watch, reactive, nextTick } from "vue"; import request from "@/utils/httpRequest"; import { ElMessage,ElMessageBox } from "element-plus"; import fcDraggable from "vuedraggable"; import formCreate from "@form-create/element-ui"; import { getFormRuleDescription } from "@/components/formCreate/src/utils/index"; // js工具 import utils from "./components/utils"; import tool from "@/utils/tool"; import config from "@/config" import { DocumentRemove, Delete } from "@element-plus/icons-vue"; // 页面loading let loading = ref(false); //上传地址 const token = tool.cookie.get("TOKEN"); const action = ref(config.BASEURL + "/system/upload/upload"); const fileList = ref([]); //上传文件列表 const dialogVisibleImg = ref(false) const dialogImageUrl = ref(&#39;&#39;) //设置请求头 const uploadHeaders = ref({ token: token, // 添加 token 到 headers }); // 设置额外字段 const uploadData = ref({ is_annex: 1, // 添加额外字段 is_annex group_id: 0, }); // 列表数据 const procedureData = ref({ current_page: 1, per_page: 10, total: 0, data: [], name: &#39;&#39;,//名称查询 }) // #region 定义工艺模板数据字段 const itemsgy = ref([])//工序列表 const formRule = ref([])//formcreate 组件 const selectedItem = ref({})// 记录当前选中工序的索引 const processLoading = ref(false)//loading const isEditor = ref(false)//用于判断是否走编辑接口 const ditVisibleGy = ref(false)//工艺模板弹窗 const formCreateOptions = ref({});//表单组件参数 //工艺form const procedureForm = ref({ good_id: &#39;&#39;,//绑定产品id name: &#39;&#39;,//工艺名称 // type_id: 0,//工艺分类 goods_name: &#39;&#39;,//绑定产品名称 item: &#39;&#39;//工序节点 }); // #endregion // #region 数据列表 function refreshProcedure() { procedureData.value.name = &#39;&#39; getProcedure() } async function getProcedure(page = 1) { if (page) { procedureData.value.current_page = page; } try { loading.value = true; let data = await request.post("/system/workmanship.workmanship_template/index", { page: procedureData.value.current_page, limit: procedureData.value.per_page, name: procedureData.value.name }); procedureData.value.current_page = data.data.current_page procedureData.value.total = data.data.total procedureData.value.data = data.data.data loading.value = false; } catch (error) { loading.value = false; } } getProcedure() // #endregion // #region 新增编辑 //点击新增 编辑 async function editProcedure(row) { itemsgy.value = [] formRule.value = [] selectedItem.value = {} fileList.value = [] if (row) { try { loading.value = true; let data = await request.post("/system/workmanship.workmanship_template/read", { id: row.id }); loading.value = false if( data.data.data.file){ fileList.value = data.data.data.file } procedureForm.value = { // goods_id: data.data.data.goods_id,//绑定产品id name: data.data.data.name,//工艺名称 // type_id: &#39;&#39;,//工艺分类 // goods_name: data.data.data.goods_name,//绑定产品名称 id: row.id } itemsgy.value = data.data.data.item for (const item of itemsgy.value) { try { let data = await request.post("/system/workmanship.workmanship_template/getItem", { id: item.id }); //value是组件的值 let content = formCreate.parseJson(data.data.form.content); const value = utils.decodeFormat(content, data.data.item, 2); item.options = value item.option = content //options是页面参数配置 formCreate.parseJson // const options = (data.data.form.options); const options = formCreate.parseJson(data.data.form.options) formCreateOptions.value = options formCreateOptions.value.submitBtn.show = false loading.value = false } catch (error) { loading.value = false; } } isEditor.value = true processLoading.value = false } catch (error) { processLoading.value = false } } else { itemsgy.value = [] procedureForm.value = { name: &#39;&#39;,//工艺名称 // type_id: &#39;&#39;,//工艺分类 goods_name: 24,//绑定产品名称 item: &#39;&#39;,//工序节点 id: &#39;&#39; } isEditor.value = false } ditVisibleGy.value = true; } // #endregion // #region 获取工序分类 const dialogVisiblexzgx = ref(false); const gymbLoading = ref(false) const gxcclist = ref([])//存储当前选中的工序列表 const tableRef = ref(null); // 绑定 scTable 组件 const gxId = ref(&#39;&#39;)//工序id // 打开工序选择弹窗,并获取工序类型数据= function changeGx() { getgxtype() } // 获取工序模板分类数据(工序类别) async function getgxtype() { try { gymbLoading.value = true let data = await request.post("/system/dictionary/getChildListByCode", { code: &#39;gongxu_type&#39; }); gxtypeList.value = data.data gxcclist.value = [] dialogVisiblexzgx.value = true; tableRef.value.clearSelection(); // 清空选中 gymbLoading.value = false; } catch (error) { gymbLoading.value = false; } } const gxtypeList = ref([])//已选中的工序 列表多选 //选择工序分类 function selectgxClass(row) { gxId.value = row.value getTemplate(1) } // #endregion // #region 获取工序模板 const tableloading = ref(false) const processesList = ref({ current_page: 1, per_page: 10, total: 0, data: [], name: &#39;&#39; })//工序模板列表 //工序form表单 const formGy = reactive({ workmanship_id: &#39;&#39;,//工艺id name: &#39;&#39;,//工序名称 piece_type: &#39;&#39;,//计件方式 dept: &#39;&#39;,//报工部门 process_quota: &#39;&#39;,//工序定额1启用2停用 price_type: &#39;&#39;,//计酬方式1计件2计工时 process_price: 0,//工序单价 deduction: 0,//不合格扣款 sort: 0,//排序 }); const bomLoading = ref(false) // 根据工序分类 ID 获取对应的工序列表 async function getTemplate(page = 1) { tableloading.value = true if (page) { processesList.value.current_page = page; } let data = await request.post("/system/processes.processes_template/index", { gongxufenlei_ids: gxId.value, limit: processesList.value.per_page, page: processesList.value.current_page, name: processesList.value.name }); tableloading.value = false processesList.value.current_page = data.data.current_page processesList.value.total = data.data.total // 更新工序列表 processesList.value.data = data.data.data nextTick(() => { recoverSelection(); // 拉完数据恢复勾选 }); } // 监听工序表格的多选事件,并更新已选中的工序数据 function handleSelectionChange(selection) { // gxcclist.value = selection // ⚡ selection 是当前页面选中的 // 我们需要同步到 gxcclist 里面,保证全局勾选 // 先删除本页数据在 gxcclist 里面的 const pageIds = processesList.value.data.map(item => item.id); gxcclist.value = gxcclist.value.filter(item => !pageIds.includes(item.id)); // 再把最新的 selection 加进去 gxcclist.value = [...gxcclist.value, ...selection]; } // 恢复勾选 function recoverSelection() { if (!tableRef.value) return; const tableData = processesList.value.data tableData.forEach((row) => { const isSelected = gxcclist.value.some(item => item.id === row.id) || itemsgy.value.some(item => item.id === row.id); tableRef.value.toggleRowSelection(row, isSelected); }); } // 是否有数据 const isFormFilled = ref(false); function handleRowClick(row) { tableRef.value.toggleRowSelection(row); } // 监听 formGy 变化 watch( formGy, (newVal) => { isFormFilled.value = Object.values(newVal).some( (value) => value !== "" && value !== null && value !== undefined ); }, { deep: true } ); const selectProcessLoading = ref(false)//loading 按钮 // 确认选择工序,并关闭弹窗 async function selectProcess() { itemsgy.value = gxcclist.value refreshGx() dialogVisiblexzgx.value = false } // #endregion //#region 刷新工序展示列表最新数据 async function refreshGx() { for (const item of itemsgy.value) { let form = {} const res = await request.post("/system/crud/read", { crud_id: 105, id: item.id }); // 格式化表单数据 const value = utils.decodeFormat( formCreate.parseJson(res.data.form.content), res.data.data, 2 ); // 表单渲染规则 form = value; const options = formCreate.parseJson(res.data.form.options); //默认给子表加上一条 form.forEach((item) => { if (item.field) { if (item.field.includes("sub_")) { if (item.value.length === 0) { item.value = [{}]; } } } }); // 编辑时删除默认数据,转而使用表单数据 // delete options.form; // 获取所有数据组件参数 const formData = formCreate.parseJson( formCreate.toJson(getFormRuleDescription(form)) ); formCreateOptions.value = options formCreateOptions.value.submitBtn.show = false // 整理格式 const postData = utils.formatForm(formData); // const psData = formData // **根据 id 匹配 itemsgy.value 数组的元素,并存入新字段 `options`** const targetItem = itemsgy.value.find(el => el.id === item.id); if (targetItem) { targetItem.options = postData; // 存入 options 字段 targetItem.option = form } } } // 记录用户点击的工序项(用于高亮显示) async function selectItem(item) { bomLoading.value = true selectedItem.value = item; // 更新选中状态 formRule.value = item.option;//选中 赋给表单 isFormFilled.value = true bomLoading.value = false } //删除对应已选工序模板 function removeField(index) { itemsgy.value.splice(index, 1); } // #endregion //#region 新增工序 弹出公共页面的新增 const ditVisibleProcedure = ref(false) const api = ref({}) const formRuleAdd = ref([]);//用于新增模板 async function addProcedure() { let res = await request.post("/system/crud/getCrudForm", { crud_id: 105 }); // 表单渲染规则 formRuleAdd.value = formCreate.parseJson(res.data.content) // 表单配置项 formCreateOptions.value = { onSubmit: async () => { // 表单验证通过后的提交方法 // 获取所有数据组件参数 let formData = formCreate.parseJson( formCreate.toJson(getFormRuleDescription(formRuleAdd.value)) ); // 整理格式 let postData = utils.formatForm(formData); await request.post("/system/crud/add", { crud_id: 105, form_data: formCreate.toJson(postData) }); ElMessage({ type: "success", message: "操作成功", }); ditVisibleProcedure.value = false }, ...formCreate.parseJson(res.data.options) }; ditVisibleProcedure.value = true } // #endregion const imageUrls = ref(&#39;&#39;) //#region 提交新增工艺 const gyloading = ref(false)//保存按钮loading async function submitProcesses() { if (gyloading.value) { return } // ✅ 防止双击或重复点击 if (!procedureForm.value.name) { ElMessage.error(&#39;请先填写工艺名称&#39;) return; } let newArray = []; // 先定义 newArray,确保在整个作用域内可访问 if (itemsgy.value.length > 0) { // 处理 itemsgy.value 数组,将 options 转换为 option 并删除 options 字段 newArray = itemsgy.value.map(item => { // 获取所有数据组件参数 let formData = formCreate.parseJson( formCreate.toJson(getFormRuleDescription(item.option)) ); const newItem = { ...item, option: utils.formatForm(formData)// 将 options 字段格式化后存入 option }; delete newItem.options;// 删除原 options 字段 return newItem;// 返回处理后的新对象 }) } if (fileList.value.length > 0) { // 提取所有上传图片的 URL imageUrls.value = fileList.value.map(file => file.full_path).join(&#39;,&#39;); } try { // 发送 POST 请求,将数据提交到后端接口 // let data = await request.post("/system/workmanship.workmanship_template/add", ); gymbLoading.value = true gyloading.value = true let url = procedureForm.value.id ? "/system/workmanship.workmanship_template/update" : "/system/workmanship.workmanship_template/add"; // 尝试提交 let data = await request.post(url, { name: procedureForm.value.name, // 工艺模板名称 // goods_id: procedureForm.value.goods_id, // 关联的商品 ID goods_name: procedureForm.value.goods_name, // 关联的商品名称 item: JSON.stringify(newArray), // 处理后的工艺项数据,转换为 JSON 字符串 id: procedureForm.value.id, file: imageUrls.value }); if (data.code === 1) { ElMessage({ message: &#39;操作成功&#39;, type: &#39;success&#39;, }) await getProcedure() gymbLoading.value = false gyloading.value = false ditVisibleGy.value = false } } catch (error) { // positionLoading.value = false; } } // #endregion //#region 上传附件 // 上传之前的处理 const uploadBefore = (file) => { const isImage = file.type.startsWith(&#39;image/&#39;); const isSizeOk = file.size / 1024 / 1024 < 20; // 限制文件大小最大为 5MB if (!isImage) { ElMessage.error(&#39;只能上传图片文件!&#39;) return false; } if (!isSizeOk) { ElMessage.error(&#39;图片大小不能超过 5MB!&#39;) return false; } gymbLoading.value = true; // 开启 loading return true; }; // 预览图片 const handlePictureCardPreview = (file) => { dialogImageUrl.value = file.full_path; // 设置预览的图片地址 dialogVisibleImg.value = true; // 显示对话框 }; //上传成功的回调 const uploadSuccess = (response) => { fileList.value.push({ original_name: response.data.original_name, path: response.data.fullurl, // 带域名的完整路径 full_path: response.data.url // 相对路径 }) ElMessage({ type: "success", message: "操作成功", }); gymbLoading.value = false }; // 上传失败的回调 const uploadError = (error, file) => { console.error("上传失败:", error, file); }; // 处理文件删除 // const handleRemove = (file, files) => { // fileList.value = files; // 更新文件列表 // ElMessage.info(`已删除文件: ${file.name}`); // }; //移除上传图纸 function delImg (index){ fileList.value.splice(index, 1); } // #endregion //#region 删除工艺模板 async function delProcedure(row) { ElMessageBox.confirm("确定要删除吗?这可能导致该数据无法找回", "警告", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }) .then(async () => { try { loading.value = true; await request.post("/system/workmanship.workmanship_template/delete", { id: row.id, }); ElMessage({ type: "success", message: "操作成功", }); getProcedure(); } catch (error) { loading.value = false; } }) .catch(() => { }); } // #endregion </script> <style scoped> .item { cursor: pointer; float: left; background: #fff; width: 100px; height: 100px; line-height: 100px; text-align: center; margin: 0 15px 15px 0; border: 1px solid #e6e6e6; display: block; transition: all 0.3s; /* 添加过渡效果 */ } /* 选中时的样式 */ .selected { border-color: #409eff; /* 选中时的边框颜色 */ background-color: #e6f7ff; /* 选中时的背景颜色 */ } .cwxzlist { display: flex; } .changeCheckbox { display: block; margin-bottom: 20px; } .addwarehouse { margin-right: 10px; } :deep(.dialogGxgy .el-dialog__headerbtn) { height: 57px !important; } .dialogtitle { display: flex; justify-content: space-between; align-items: center; } .fullscreen-icon { cursor: pointer; font-size: 18px; color: #606266; transition: color 0.2s; } .fullscreen-icon:hover { color: #409eff; } .dialog-icons { display: flex; align-items: center; gap: 10px; } .gybs { display: flex; align-items: center; width: 100%; } .gyheader { margin-right: 20px; display: flex; align-items: center; width: 24%; } .card_gy { margin: 10px; min-height: 500px; position: relative; } ::v-deep.card_gy .el-card__body { width: 100%; } .tzpx { height: 400px; overflow-y: scroll; scrollbar-width: none; /* Firefox 隐藏滚动条 */ } .slx { display: flex; } .sortable-item { width: 100%; padding: 15px 20px; background: #ffffff; border: 1px solid #e4e7ed; border-radius: 8px; cursor: move; margin-bottom: 12px; transition: all 0.3s ease; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); display: flex; align-items: center; justify-content: center; font-size: 14px; color: #333; } .sortable-item:hover { background-color: #f5f5f5; border-color: #dcdfe6; color: #409eff; } .sortable-item:active { transform: scale(0.98); } .sortable-item .drag-handle { cursor: move; font-size: 18px; color: #999; margin-right: 12px; } .sortable-item .drag-handle:hover { color: #409eff; } .selected { background: #409eff; /* 选中状态变蓝色 */ color: white; font-weight: bold; } .item_left { margin-right: 8px; } .item_content { flex: 1; /* 占据中间可用空间 */ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .item_right { margin-left: 8px; margin-left: auto; /* 自动将右侧图标推到最右 */ cursor: pointer; } .btgp { display: flex; justify-content: center; position: absolute; bottom: 3%; left: 50%; transform: translateX(-50%); } .gyform { padding: 10px; } .dowt { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #e4e7ed; border-bottom: 1px solid #e4e7ed; padding: 12px 15px; } .route { margin-top: 30px; } .image-preview-list { width: 100%; text-align: center; padding-top: 30px; /* overflow-x: scroll; */ /* overflow: scroll; */ } .image-preview-list::-webkit-scrollbar { display: none; /* Safari 和 Chrome 隐藏滚动条 */ } /* ::v-deep .el-upload-list { display: flex; } */ .lf_aside { border-left: 1px solid #e4e7ed; display: flex; justify-content: center; } .upbox { width: 100%; display: flex; /* flex-wrap: wrap; */ justify-content: center; flex-direction: column-reverse; align-items: center; } .wdg { width: 90%; border: 1px solid #e4e7ed; height: 50px; border-radius: 5px; display: flex; align-items: center; padding: 0 10px; justify-content: space-between; margin-top: 10px; } .wg_lf { display: flex; align-items: center; cursor: pointer; } .weds { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; width: 80px; display: flex; margin-left: 5px; } </style> 这是我的代码 我的代码现在 handleSelectionChange 勾选列表有问题 如果我勾选多个 然后重新获取列表就会加载不上已勾选的状态
08-20
<think> 我们有一个需求:当父组件被点击时,控制其子组件中的input(可能是checkbox或radio)的选中状态,并且能够切换(即点击父组件,如果子组件是未选中则变为选中,反之则取消选中)。同时,我们还要注意,如果子组件被点击,不应该触发父组件的点击事件(避免事件冒泡导致的多次触发)。 从引用中,我们可以借鉴一些思路: 1. 引用[1]展示了全选/取消全选的功能,它通过一个复选框(#checkAll)来控制其他复选框的选中状态。但这里我们不是用另一个复选框,而是用一个父元素(可能是div等)来控制。 2. 引用[2]展示了如何通过点击事件来实现radio的选中和取消选中(通常radio是不能取消的,但通过自定义属性实现了)。但我们的需求是父元素控制子元素。 因此,我们可以这样设计: - 给父元素绑定点击事件。 - 在事件处理函数中,找到父元素内的所有目标input(比如checkbox或radio)并改变它们的选中状态。 - 注意:我们可能不希望子元素本身的点击事件冒泡到父元素,因为这样会导致两次触发(一次子元素自身改变,一次父元素改变它)。所以我们需要在子元素上阻止事件冒泡。 但是,这里有一个问题:如果父元素被点击,我们改变子元素选中状态,那么当点击子元素时,由于事件冒泡,父元素的点击事件也会触发,这样就会导致子元素被改变两次(一次是子元素自己的默认行为,一次是父元素的事件处理函数)。因此,我们需要: 1.子元素的点击事件中阻止事件冒泡(event.stopPropagation()),这样点击子元素就不会触发父元素的点击事件。 2. 或者,在父元素的点击事件中,判断事件的目标(event.target)是否是子元素,如果是,则不执行操作。但这样当点击子元素时,父元素事件也会触发,只是我们判断目标后跳过,但这样不如第一种方式好。 另外,对于checkbox,我们通常希望点击子元素checkbox时,它自己会切换状态(这是浏览器默认行为),同时我们又不希望父元素的事件来干扰。所以,我们可以在父元素的事件处理中,排除对子元素input的点击的处理(通过判断事件目标是否为input,如果是则返回)。但是,如果父元素除了包含input还有其他内容,我们可能希望点击父元素的其他部分也能控制input。 因此,我们可以这样设计结构: ```html <div class="parent"> 点击我控制下面的复选框 <input type="checkbox" class="child"> 选项1 <input type="checkbox" class="child"> 选项2 </div> ``` 当点击父元素(div)时,我们切换所有子元素选中状态。但是,当我们点击input时,我们不希望父元素的事件触发,所以我们在input上阻止冒泡。 然而,这里还有一个问题:点击input时,除了改变自己的状态,还会冒泡(如果我们不阻止的话)到父元素,然后父元素又会去改变所有子元素的状态(包括刚刚被点击的那个),这样就会导致状态被覆盖(比如,点击一个未选中input,它自己会变成选中,然后父元素事件触发,将所有的子元素都设置为选中,这看起来似乎没有影响,但如果我们之前已经有一个是选中的,点击另一个,那么父元素事件会把之前选中的也变成选中,这通常不是我们想要的)。因此,我们必须在子元素上阻止事件冒泡。 步骤: 1. 给父元素绑定点击事件,在事件处理函数中,切换所有子元素checkbox的选中状态。 2.子元素绑定点击事件,在事件处理函数中阻止事件冒泡。 但是,如果我们希望点击父元素内的文本(或其他元素)来控制子元素,而点击子元素本身只由子元素自己的事件处理,那么这样是可行的。 然而,我们注意到,子元素checkbox本身就有默认行为,所以我们不需要再给它绑定事件来改变状态(默认行为已经可以改变状态)。我们只需要阻止它的事件冒泡,以免触发父元素的事件。 所以,代码可以这样写: ```javascript // 父元素点击事件 $(&#39;.parent&#39;).on(&#39;click&#39;, function(event) { // 排除点击的是input的情况,因为input已经阻止冒泡,所以这里其实不会点击到input,但为了安全,也可以判断一下 // 不过,由于我们在子元素input上阻止了冒泡,所以事件目标不会是input,除非我们点击的是父元素内的其他元素 // 因此,我们直接切换所有子元素checkbox的状态 var $children = $(this).find(&#39;.child&#39;); // 获取当前第一个子元素选中状态(或者我们可以根据父元素的点击来切换每一个子元素的状态:取反) // 这里我们采用:将每一个子元素的状态取反 $children.each(function() { var checked = $(this).prop(&#39;checked&#39;); $(this).prop(&#39;checked&#39;, !checked); }); }); // 阻止子元素的事件冒泡 $(&#39;.child&#39;).on(&#39;click&#39;, function(event) { event.stopPropagation(); }); ``` 但是,上述代码有一个问题:当我们点击父元素时,它会切换每一个子元素的状态(取反)。这通常是我们想要的。但是,如果我们希望点击父元素时,将所有的子元素都设置成与当前第一个子元素相反的状态(比如全选或全不选),那么我们可以用另一种方式: ```javascript $(&#39;.parent&#39;).on(&#39;click&#39;, function() { var $children = $(this).find(&#39;.child&#39;); // 判断当前是否所有子元素都被选中,然后设置成相反的状态(全部选中或全部取消) // 或者我们也可以简单地将所有子元素的状态取反(每个独立取反) // 根据需求,如果希望是独立取反,就用上面的each循环;如果希望是统一设置为全选或全不选,那么可以: var allChecked = $children.length === $children.filter(&#39;:checked&#39;).length; $children.prop(&#39;checked&#39;, !allChecked); }); ``` 两种方式根据需求选择。 但是,我们的需求是:当父组件被点击时,子组件input选中或取消选中。这里并没有明确是每个子组件独立切换,还是统一设置。从问题描述来看,可能是统一设置(类似于全选/全不选),但也可以理解为每个都切换。所以我们需要明确。 另外,对于radio,我们通常只能选中一个,所以如果我们用切换每个的状态,那么可能就会导致多个radio被选中(如果之前有选中的,我们切换它就会取消,同时切换另一个就会选中,这样就有两个被选中了),这不符合radio单选的行为。因此,对于radio,我们通常不会用切换状态的方式,而是由父元素来设置选中的某个radio?但是,需求是父组件被点击时,子组件input选中或取消选中。对于radio,取消选中可能不是常见需求。 所以,我们需要区分checkbox和radio。 但是,问题没有明确是checkbox还是radio。所以我们可以提供两种思路,或者根据子组件的类型分别处理。 不过,从问题描述来看,用户可能是希望实现类似全选的功能(子组件是checkbox),也可能是其他。这里我们假设子组件是checkbox,因为如果是radio,取消选中并不常见,而且通常需要额外的逻辑(如引用[2]所示)。 因此,我们先按照checkbox来实现,同时给出radio的注意事项。 下面是一个完整的示例,假设是checkbox: HTML: ```html <div class="parent" style="border:1px solid #ccc; padding:10px; cursor:pointer;"> 点击此区域切换所有子复选框 <br> <input type="checkbox" class="child"> 选项1 <input type="checkbox" class="child"> 选项2 <input type="checkbox" class="child"> 选项3 </div> ``` jQuery: ```javascript $(function() { // 父元素点击事件:切换所有子复选框的状态(每个独立取反) $(&#39;.parent&#39;).on(&#39;click&#39;, function() { $(this).find(&#39;.child&#39;).each(function() { $(this).prop(&#39;checked&#39;, !$(this).prop(&#39;checked&#39;)); }); }); // 阻止子复选框的点击事件冒泡 $(&#39;.child&#39;).on(&#39;click&#39;, function(e) { e.stopPropagation(); }); }); ``` 这样,点击父元素区域(除了复选框本身)时,每个子复选框都会独立切换状态。而点击复选框本身时,由于阻止了冒泡,父元素事件不会被触发,所以子复选框按照默认行为切换(并且我们不需要在事件处理函数中做任何改变状态的事情,因为浏览器默认会切换)。 如果我们希望点击父元素时,统一设置子复选框为全选或全不选(而不是独立切换),那么可以修改父元素的事件处理函数: ```javascript $(&#39;.parent&#39;).on(&#39;click&#39;, function() { var $children = $(this).find(&#39;.child&#39;); // 检查是否全部选中,如果是则全部取消,否则全部选中 var allChecked = $children.length === $children.filter(&#39;:checked&#39;).length; $children.prop(&#39;checked&#39;, !allChecked); }); ``` 这样,点击父元素就会在全选和全不选之间切换。 用户可以根据实际需求选择。 对于radio,我们通常不会这样操作,因为radio是单选。如果确实需要,可以这样:点击父元素时,取消所有radio的选中。但是选中一个radio通常是通过点击radio本身。所以对于radio,我们可能不需要这样的功能。 因此,针对问题,我们给出checkbox的实现。 注意:在事件处理中,我们使用`prop`来设置`checked`属性,因为`attr`在有些情况下(如初始设置后)不会生效。 总结代码: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>父元素控制子复选框</title> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $(function() { // 方式1:独立切换每个子复选框的状态 $(&#39;.parent&#39;).on(&#39;click&#39;, function() { $(this).find(&#39;.child&#39;).each(function() { $(this).prop(&#39;checked&#39;, !$(this).prop(&#39;checked&#39;)); }); }); // 方式2:全选/全不选切换(注释掉方式1,使用方式2) /* $(&#39;.parent&#39;).on(&#39;click&#39;, function() { var $children = $(this).find(&#39;.child&#39;); var allChecked = $children.length === $children.filter(&#39;:checked&#39;).length; $children.prop(&#39;checked&#39;, !allChecked); }); */ $(&#39;.child&#39;).on(&#39;click&#39;, function(e) { e.stopPropagation(); }); }); </script> </head> <body> <div class="parent" style="border:1px solid #ccc; padding:10px; cursor:pointer;"> 点击此区域切换所有子复选框(独立切换) <br> <input type="checkbox" class="child"> 选项1 <input type="checkbox" class="child"> 选项2 <input type="checkbox" class="child"> 选项3 </div> </body> </html> ``` 用户可以根据需要选择方式1或方式2。 注意:上面的代码中,方式1和方式2只能选择一种,不要同时使用。 另外,如果子元素中有其他需要点击的元素,并且不希望阻止冒泡,那么需要调整选择器,确保只对需要阻止冒泡的元素(即checkbox)进行阻止。 以上就是实现父组件被点击时子组件input(checkbox)选中或取消选中的方法。 </think> 在jQuery中实现父组件点击时控制子组件input选中/取消选中的功能,关键在于事件委托和状态切换。以下是两种实用实现方案: ### 方案1:基础实现(推荐) ```html <div class="parent"> 点击此区域切换子选项 <div> <input type="checkbox" class="child"> 选项1 <input type="checkbox" class="child"> 选项2 </div> </div> <script> $(function() { // 父组件点击事件 $(&#39;.parent&#39;).on(&#39;click&#39;, function(e) { // 排除直接点击子元素的情况 if ($(e.target).is(&#39;.child&#39;)) return; // 切换所有子元素选中状态 $(&#39;.child&#39;, this).each(function() { const isChecked = $(this).prop(&#39;checked&#39;); $(this).prop(&#39;checked&#39;, !isChecked); }); }); }); </script> ``` ### 方案2:包含状态同步(含取消冒泡) ```html <div class="parent-box"> 父控制区 <div> <input type="checkbox" class="child-input"> 苹果 <input type="checkbox" class="child-input"> 香蕉 </div> </div> <script> $(function() { // 父元素点击处理 $(&#39;.parent-box&#39;).click(function() { const $children = $(this).find(&#39;.child-input&#39;); const allChecked = $children.length === $children.filter(&#39;:checked&#39;).length; // 切换选中状态(全选/全不选) $children.prop(&#39;checked&#39;, !allChecked); }); // 阻止子元素事件冒泡 $(&#39;.child-input&#39;).click(function(e) { e.stopPropagation(); }); }); </script> ``` ### 关键实现原理 1. **事件委托**:通过`$(&#39;.parent&#39;).on(&#39;click&#39;)`监听父元素点击 2. **状态切换**: - 使用`.prop(&#39;checked&#39;)`获取当前状态 - 通过`.prop(&#39;checked&#39;, !isChecked)`取反状态 3. **冒泡控制**: - `e.stopPropagation()`阻止子元素事件冒泡到父元素 - 或通过`if($(e.target).is(&#39;.child&#39;)) return`过滤子元素事件 ### 注意事项 1. 使用`prop()`而非`attr()`操作checked属性 2. 父元素需设置CSS样式`cursor: pointer`提升可用性 3. 对于radio单选按钮组,需要额外处理互斥逻辑[^2] ### 实际应用场景 1. 表格行点击选中该行复选框 2. 分类目录控制下属所有条目 3. 购物车全选/取消功能 4. 树形结构父节点控制子节点 ---
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值