点击打印留样标签弹出一个popup弹出层组件,关闭这个组件,点击查询按钮
- 发布:2026-06-18 10:41
- 更新:2026-06-18 17:23
- 阅读:126
产品分类: uniapp/小程序/钉钉
PC开发环境操作系统: Windows
PC开发环境操作系统版本号: Windows11
第三方开发者工具版本号: 钉钉开发者工具3.9.22.0
基础库版本号: 1.25.10
项目创建方式: CLI
CLI版本号: 1.0.0
操作步骤:
预期结果:
刷新列表,获取第一页数据
刷新列表,获取第一页数据
实际结果:
刷新列表,获取第一页数据之后,popup弹出层组件又出现了,相当于点击了打印留样标签按钮
刷新列表,获取第一页数据之后,popup弹出层组件又出现了,相当于点击了打印留样标签按钮
bug描述:
钉钉原生没有这个问题
大模型搜索:
根本原因:UniApp 的转换机制
这个 bug 的核心原因是 UniApp 在将 Vue 代码转换为小程序代码时,其事件绑定机制和 DOM 渲染逻辑与钉钉 Android 端的 WebView 在某些场景下产生了兼容性问题。
简单来说,UniApp 的跨端框架为了兼容多平台,需要对事件监听和视图更新做一层封装和转换。在 Android 钉钉小程序这个特定组合下,这层转换可能没能完全处理好弹窗关闭动画期间,触摸事件被"穿透"到底层按钮的情况。而在 iOS 端,因为 WebView 和事件处理机制不同,这个问题就不容易出现。
你们找些安卓机器测试下就可以了,我用自己写的一个弹出层v-if显示隐藏也会出现这个问题,还有就是点击按钮不要弹出层,直接调用后端接口,点击查询之后又从新执行了点击按钮调接口的操作了,这个问题是在安卓手机上偶尔出现,但是版本比较低的安卓系统出现次数多一些
知识库无法支撑。请补充HBuilderX版本、相关代码及详细报错信息,以便精准排查钉钉小程序安卓端弹窗异常问题。
欢迎到专业群( uni-app 官方技术交流群 1 ) 咨询,群中有相关专业的管理员和群友。
好的,收到这个 bug 反馈了。作为 DCloud 官方人员,我来对这个反馈进行评审。
首先,评价一下这个 bug 反馈的效果。
这个反馈整体质量不错,尤其是在问题描述部分,提供了非常关键的信息,值得肯定。
-
问题描述清晰且深入:反馈者没有停留在“弹窗自动弹出”这个表面现象,而是主动进行了排查和分析。他明确指出“钉钉原生没有这个问题”,并且测试了使用
v-if自己写的弹出层也会复现,更重要的是,他指出了“点击查询之后又重新执行了点击按钮调接口的操作”。这非常关键,它将问题从“popup 组件异常”精准地定位到了“事件穿透/重复触发”这个更核心的机制上。这为问题的定位节省了大量时间。 -
环境信息提供了关键线索:他提到了“安卓手机”、“版本比较低的安卓系统出现次数多一些”,这为判断这是一个特定平台(Android钉钉小程序)和特定 WebView 内核版本的兼容性问题,提供了很有价值的线索。
其次,分析一下 bug 反馈内容的完整性,有几处需要补充:
-
缺少代码示例:这是最关键的缺失。反馈中提到了“打印留样标签”按钮和“查询”按钮,以及一个 popup 组件。但完全没有提供任何代码片段。官方人员需要看到:
- 这两个按钮是如何绑定事件的?
- popup 组件是如何使用和关闭的(通过
ref调用close()还是v-model控制)? - 页面布局是怎样的?按钮和 popup 组件是否有层级或位置上的重叠?
没有代码,就无法判断是否存在事件冒泡、v-if/v-show使用不当、或者 ref 引用错误等常见问题。
-
复现步骤不够具体:虽然逻辑清晰,但缺少具体的操作细节。例如,关闭 popup 组件后,是立即点击查询按钮,还是间隔了一段时间?复现是“偶尔出现”,那么大概的操作频率是多少?需要补充一个更精确的操作路径。
-
分类信息缺失版本号:只写了“HBuilderX版本 正式版”,没有提供具体的版本号,比如 4.87 或 5.07。也缺少钉钉客户端的版本和 Android 系统版本。这些对排查兼容性问题至关重要。
然后,判断这个 bug 是否成立。
这个 bug 是成立的,而且反馈者的分析很可能是对的。
这不是一个 uni-popup 组件本身的显示/隐藏 bug,而是一个事件处理机制的兼容性问题。反馈者描述的“点击查询之后又重新执行了点击按钮调接口的操作”是典型的“事件穿透”现象。
在小程序环境中,弹出层(popup)的关闭往往伴随着动画效果。在 Android 的特定 WebView 内核下,当 popup 的遮罩层和内容在关闭动画期间迅速消失时,用户紧接着点击下方“查询”按钮的触摸事件,可能会被错误地分发到消失前该位置下方的“打印留样标签”按钮上。低版本 Android 系统出现次数更多,也符合这个猜测,因为旧版 WebView 的事件处理机制可能更不完善。
从知识库的对话记录中,也能看到类似的问题模式。有用户反馈过 uni-popup 在微信小程序上,点击遮罩层关闭后重复几次,会导致页面按钮无法点击。这说明 popup 组件在小程序环境下的动画和事件处理,确实存在一些需要打磨的边界情况。
最后,提供一些分析和建议:
-
根本原因:这个问题的根源在于 UniApp 框架在将 Vue 的弹出层逻辑转换为小程序原生的
animation和hidden等属性时,其动画时序与 Android 钉钉客户端 WebView 的事件分发机制在特定场景下产生了冲突,导致事件穿透。 -
解决方案建议:
- 事件防抖/节流:这是最直接有效的临时方案。为“打印留样标签”按钮的点击事件添加防抖处理(例如使用 lodash 的
debounce或自己写一个简易的),限制它在短时间内(比如 300ms)只能被触发一次。这可以防止 popup 关闭动画期间的事件穿透。 - 延迟关闭与事件处理:在 popup 的关闭逻辑中,先隐藏内容,延迟一小段时间(比如 200ms)再彻底移除遮罩层,或者延迟执行查询按钮的逻辑。但这种方法可能会影响用户体验,让操作感觉不跟手。
- 使用
v-if替代v-show:如果 popup 是使用v-show控制,可以尝试改用v-if配合nextTick。v-if会销毁和重建 DOM 元素,能更彻底地避免事件穿透,但可能会带来性能开销和动画不流畅的问题。 - 完善代码示例并提交:建议反馈者按照上面的要求,补充一个最简化的、可运行的 demo 代码,并附上具体的 HBuilderX 版本、钉钉版本和 Android 系统版本,然后提交一个完整的 issue。这样官方人员可以更精确地复现和修复这个底层框架的兼容性问题。
- 事件防抖/节流:这是最直接有效的临时方案。为“打印留样标签”按钮的点击事件添加防抖处理(例如使用 lodash 的
1***@163.com (作者)
/>
<view v-show="isshow1" class="left">
<div @click="handlefocus1">
<up-search
v-model="query.materialTypeName"
placeholder="请选择材质" clearabled="true" showAction="false" inputStyle="{ 'pointer-events': 'none' }" />
</div>
<div @click="handlefocus2">
<up-search
v-model="query.specificationName"
placeholder="请选择规格型号" clearabled="true" showAction="false" inputStyle="{ 'pointer-events': 'none' }" />
</div>
<MachineAndFurnaceNumber query="query" @updateMachineFurnace="handleSearch"
ref="MachineAndFurnaceRef"
/>
<!-- <PatetimePicker :query="query" @updateFn="handleSearch" /> -->
<PatetimePicker query="query" @updateFn="handleSearch"
titleName="分卷操作"
startTime="cuttingTimeStart"
endTime="cuttingTimeEnd"
/>
</view>
</view>
<view class="right">
<up-button
type="primary"
@click="handleSearch"
size="small"
shape="circle"
class="first"
>搜索</up-button
>
<up-button
type="primary"
@click="handlecancel"
size="small"
shape="circle"
>重置</up-button
>
</view>
<scanCodeList :resultPakage="resultPakage" />
</view>
<view class="upCcon">
<up-icon name="iconName" size="50rpx"
color="#3c9cff"
@click="handleicon"
></up-icon>
</view>
<scroll-view
class="commonLim-listView"
scroll-y
@scrolltolower="onScrollToBottom"
>
<view v-show="state.rows.length">
<view
v-for="(item, index) in state.rows" key="item.id" class="commonLim-listView-item"
>
<view class="commonLim-listView-item-title">
<label>{{ item.batchNo }}</label>
<text class="mgr40">工序:{{ item.processName }}</text>
<text>序号:{{ index + 1 }}</text>
</view>
<view class="commonLim-listView-item-content">
<view class="half width100">
<label>随工单编号:</label>
<text>{{ item.trackingNo }}</text>
<label v-if="item.isCancellation == 1" style="color: #f56c6c"
>(作废)</label
>
</view>
<view class="half width100">
<label>分卷状态:</label>
<text>{{ reelScomtatus(item.reelStatus) }}</text>
</view>
<view class="half width100">
<label>物料编码:</label>
<text>{{ item.materialCode }}</text>
</view>
<view class="half width100">
<label>材质:</label>
<text>{{ item.materialTypeName }}</text>
</view>
<view class="half width100">
<label>规格型号:</label>
<text>{{ item.specificationName }}</text>
</view>
<view class="half width100">
<label>炉号:</label>
<text>{{ item.heatNo }}</text>
</view>
<view class="half width100">
<label>机台编号:</label>
<text>{{ item.machineNo }}</text>
</view>
<view class="half width100">
<label>炉管号:</label>
<text>{{ item.furnaceTubeNo }}</text>
</view>
<view class="half width100">
<label>分轮编号:</label>
<text>{{ item.roundNo }}</text>
</view>
<view class="half width100">
<label>分轮重量:</label>
<text>{{ item.roundWeight }}</text>
</view>
<view class="half width100">
<label>内部编号:</label>
<text>{{ item.sampleNo }}</text>
</view>
<!-- <view class="half width100">
<label>操作人:</label>
<text>{{ item.operatorName }}</text>
</view>
<view class="half width100">
<label>操作日期:</label>
<text>{{ item.operationDate }}</text>
</view> -->
<view
class="half width100"
style="color: #f56c6c"
v-if="item.isCancellation == 1"
>
<label>作废原因:</label>
<text>{{ item.reason }}</text>
</view>
<view class="half width100">
<label>分卷人:</label>
<text>{{ item.cuttingBy }}</text>
</view>
<view class="half width100">
<label>分卷操作日期:</label>
<text>{{ item.cuttingTime }}</text>
</view>
<view class="half width100">
<label>质检人员:</label>
<text>{{ item.inspectionBy }}</text>
</view>
<view class="half width100">
<label>质检时间:</label>
<text>{{ item.inspectionTime }}</text>
</view>
<view class="half width100">
<label>称重人:</label>
<text>{{ item.weighingBy }}</text>
</view>
<view class="half width100">
<label>称重日期:</label>
<text>{{ item.weighingTime }}</text>
</view>
<view class="half width100">
<label>入库人:</label>
<text>{{ item.stockBy }}</text>
</view>
<view class="half width100">
<label>入库日期:</label>
<text>{{ item.stockCompleteTime }}</text>
</view>
</view>
<view class="commonLim-listView-item-button">
<view class="box">
<up-button
type="primary"
text="打印留样标签"
size="small"
@click="handleprint2(item)" customStyle="{ padding: '0 30rpx' }" ></up-button>
<up-button
type="error"
text="异常查看"
size="small"
@click="handleLook(item)" customStyle="{ padding: '0 30rpx' }" ></up-button>
</view>
</view>
</view>
</view>
<up-empty mode="data" v-show="state.rows.length == 0"> </up-empty>
<up-loadmore
v-show="state.rows.length" status="loadStatus" icon="true" loadmore-text="轻轻上拉获取更多"
loading-text="努力加载中..."
nomore-text="没有更多了"
@loadmore="handleloadMore"
/>
</scroll-view>
<printComponent imgSrc="imgSrc" showReject="showchenpinReject" @closeFn="showchenpinReject = false"
titleName="查看二维码"
/>
</view>
<up-popup show="abnormalShow" mode="bottom"
closeOnClickOverlay
overlayOpacity="1"
@close="abnormalShow = false"
>
<view
style="
text-align: center;
color: #f56c6c;
margin-top: 20rpx;
font-size: 36rpx;
"
>异常信息</view
>
<view class="boxTables" style="padding: 30rpx; border-radius: 30rpx">
<view class="left">
<view class="common">操作人:</view>
<view class="common">
{{ abnormalObject.operatorName }}
</view>
</view>
<view class="left">
<view class="common">操作日期:</view>
<view class="common">
{{ abnormalObject.operationDate }}
</view>
</view>
<view class="left">
<view class="common">异常原因:</view>
<view class="common">
{{ abnormalObject.reason }}
</view>
</view>
<view class="left">
<view class="common">异常审批人:</view>
<view class="common">
{{ abnormalObject.approverName }}
</view>
</view>
<view class="left">
<view class="common">异常操作日期:</view>
<view class="common" style="border-bottom: 2rpx solid #ccc">{{
abnormalObject.operationDate
}}</view>
</view>
</view>
</up-popup>
</view>
</template>
<script setup>
import { FinishedProductLabel } from "../../commonApi/index.js";
import { pagedynamicdate } from "./api.js";
import { useMemberStore } from "@/stores/modules/member";
import { baseURL, reelLists } from "@/utils/index.js";
const memberStore = useMemberStore();
const MachineAndFurnaceRef = ref(null);
const loadStatus = ref("loadmore");
const iconName = ref("arrow-down");
const showchenpinReject = ref(false);
const abnormalShow = ref(false);
const abnormalObject = ref({});
const imgSrc = ref([]);
const isshow1 = computed(() => {
if (iconName.value == "arrow-down") {
return false;
} else {
return true;
}
});
const reelScomtatus = computed(() => {
return (reelStatus) => {
return reelLists[reelStatus];
};
});
const handleicon = () => {
if (iconName.value == "arrow-down") {
iconName.value = "arrow-up";
} else {
iconName.value = "arrow-down";
}
};
const Initialriqi = new Date().toISOString().split("T")[0];
const query = ref({
page: 0,
size: 10,
operationDateStart: Initialriqi,
operationDateEnd: Initialriqi,
cuttingTimeStart: Initialriqi,
cuttingTimeEnd: Initialriqi
});
const state = reactive({
rows: [],
totals: 0
});
const loadData = async () => {
const res = await pagedynamicdate(query.value);
state.rows = [...state.rows, ...res.content];
state.totals = res.totalElements;
if (state.rows.length < state.totals) {
loadStatus.value = "loadmore"; // 仍可继续上拉
} else {
loadStatus.value = "nomore"; // 已全部加载
}
};
const Consistentlogic = async () => {
try {
uni.showLoading({
title: "加载中"
});
state.rows = [];
state.totals = 0;
await loadData();
} finally {
uni.hideLoading();
}
};
const handleSearch = () => {
query.value.page = 0;
Consistentlogic();
};
const handlecancel = () => {
query.value = {
page: 0,
size: 10
};
MachineAndFurnaceRef.value.luguanhaoiList = [];
Consistentlogic();
};
const handleloadMore = async () => {
// 防止重复请求
if (loadStatus.value === "loading") return;
loadStatus.value = "loading";
// 判断是否还有更多数据
if (state.rows.length < state.totals) {
loadStatus.value = "loadmore"; // 仍可继续上拉
query.value.page += 1; // 为下一次请求准备页码
try {
await loadData();
} catch (error) {
console.log("加载数据失败", error);
}
} else {
loadStatus.value = "nomore"; // 已全部加载
}
};
function onScrollToBottom() {
// 当 scroll-view 触底且状态为 loadmore 时手动调用
if (loadStatus.value === "loadmore") {
handleloadMore();
}
}
//扫码检索需要执行的函数
const resultPakage = (value) => {
query.value.keyWord = value;
handleSearch();
};
const handleprint2 = async (item) => {
try {
imgSrc.value = null;
uni.showLoading({
title: "加载中"
});
const res = await FinishedProductLabel(item.id, 2);
showchenpinReject.value = true;
uni.hideLoading();
if (res) {
imgSrc.value = res.map(
(url) =>
${baseURL}/zgjmAPI/file/preview?filePath=${encodeURIComponent(url)}
);
}
} catch (err) {
uni.hideLoading();
}
};
const handlefocus1 = () => {
uni.navigateTo({
url: /pages/searchcaizhi/searchcaizhi?from=caizhi
});
};
const handlefocus2 = async () => {
if (query.value.materialTypeId) {
uni.navigateTo({
url: /pages/searchcaizhi/searchcaizhi?from=guigexinghao&pId=${query.value.materialTypeId}
});
} else {
uni.showToast({
icon: "none",
title: "请选择材质"
});
}
};
const handleLook = (item) => {
abnormalObject.value = item;
abnormalShow.value = true;
};
onLoad(() => {
handleSearch();
});
onShow(() => {
if (memberStore.from_Page == "caizhi") {
query.value.materialTypeId = memberStore.zemId;
query.value.materialTypeName = memberStore.zemName;
query.value.specificationId = null;
query.value.specificationName = null;
handleSearch();
} else if (memberStore.from_Page == "guigexinghao") {
query.value.specificationId = memberStore.zemId;
query.value.specificationName = memberStore.zemName;
handleSearch();
}
memberStore.zemId = "";
memberStore.zemName = "";
memberStore.from_Page = "";
});
</script>
<style lang="scss" scoped>
.boxTables {
.left {
.common:nth-of-type(1) {
background: #f56c6c;
}
}
}
</style>
1***@163.com (作者)
这个问题影响很大,一个页面有搜索条件,有查询按钮,有列表,列表中有一些按钮,比如删除,编辑,设置都很正常。生产环境我们当时有一个剪线按钮,客户需要多次点击剪线按钮,每次点击剪线就会调后端接口,安卓手机上点击多次剪线之后,在点击查询按钮调接口获取第一页数据,然后就出现第一条数据的剪线按钮就被点击了,这个很致命,这种页面功能是很普遍,影响太大了,请官方人员解决下这个问题,谢谢
