① 用户管理「当日执行」弹窗:状态列的「成功/失败」与「已删除」两个标签原在同一行
(td 带 whitespace-nowrap,列宽 w-[60px]),在窄列下被挤到溢出、视觉上串到
「说明」列。改为纵向排列(flex-col + gap-1,各占一行),表头宽度放宽到 w-[72px]。
② CDK 管理复制「提示成功但实际粘贴不出内容」:
根因是 copyToClipboard 把临时 textarea 挂到 document.body,而复制按钮位于
AppDialog(reka-ui)弹窗内——DialogContent 用 FocusScope 锁定焦点,document 上的
focusin 监听发现焦点跑到 dialog 容器外就立即抢回,导致 ta.focus()/select() 失效,
选区为空;execCommand('copy') 仍返回 true,于是「显示已复制、实际为空」。
修复:
- textarea 优先挂到当前打开的 dialog 容器内(role="dialog" 中最靠后可见者),
避免焦点被 FocusScope 抢回;
- 复制前校验选区确实落在 textarea 上(selectionStart/End 与 activeElement),
校验不过则不再盲信 execCommand 的返回值,转而走 Clipboard API 兜底;
- 移除 textarea 改为按实际父节点(不再假设是 body)。
该修复同时惠及所有位于弹窗内的复制按钮(CDK 单个/全部复制、待审学校等)。
注:AdminUsers 的设备码复制(copyDeviceCode)未在本次范围内,保持原样。
This commit is contained in:
@@ -6,8 +6,25 @@
|
||||
* 若先 await Clipboard API 被拒(移动端 WebView 常见 NotAllowedError),
|
||||
* 手势上下文已丢失,后续 execCommand 会静默失败(返回 false 但剪贴板无内容)。
|
||||
*
|
||||
* 弹窗内复制的坑:AppDialog(reka-ui)会用 FocusScope 锁定焦点——
|
||||
* document 上的 focusin 监听发现焦点跑到 dialog 容器之外时会立即抢回。
|
||||
* 若把临时 textarea 挂到 document.body(容器外),ta.focus() 会立刻被抢走,
|
||||
* 选区失效,execCommand('copy') 仍返回 true 但复制的是空内容
|
||||
*(表现为「提示已复制,实际粘贴不出东西」,且只发生在弹窗内的复制按钮)。
|
||||
* 因此:优先挂到当前打开的 dialog 容器内;并显式校验选区是否真的取到文本。
|
||||
*
|
||||
* 返回是否复制成功
|
||||
*/
|
||||
function findActiveDialogContainer(): HTMLElement | null {
|
||||
const dialogs = document.querySelectorAll<HTMLElement>('[role="dialog"]')
|
||||
// 取最后一次渲染(DOM 中靠后)且可见的那个
|
||||
for (let i = dialogs.length - 1; i >= 0; i--) {
|
||||
const el = dialogs[i]
|
||||
if (el && el.offsetParent !== null) return el
|
||||
}
|
||||
return dialogs.length > 0 ? dialogs[dialogs.length - 1] : null
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
// 1) 同步路径:隐藏 textarea + execCommand(用户手势内,兼容性最好)
|
||||
const ta = document.createElement('textarea')
|
||||
@@ -16,17 +33,24 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
// font-size ≥ 16px 避免 iOS focus 时触发页面缩放
|
||||
ta.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;font-size:16px;border:0;padding:0;margin:0;'
|
||||
ta.setAttribute('readonly', '')
|
||||
document.body.appendChild(ta)
|
||||
ta.focus()
|
||||
ta.select()
|
||||
ta.setSelectionRange(0, text.length)
|
||||
// 关键:挂到 dialog 容器内(若有弹窗打开),否则焦点会被 FocusScope 抢回导致为空复制
|
||||
const host: HTMLElement = findActiveDialogContainer() || document.body
|
||||
host.appendChild(ta)
|
||||
let ok = false
|
||||
try {
|
||||
ok = document.execCommand('copy')
|
||||
ta.focus()
|
||||
ta.select()
|
||||
ta.setSelectionRange(0, text.length)
|
||||
// 校验选区确实落在本 textarea 上(焦点被抢时 selectionStart/End 会异常)
|
||||
const selectionValid =
|
||||
ta.selectionStart === 0 && ta.selectionEnd === text.length && document.activeElement === ta
|
||||
if (selectionValid) {
|
||||
ok = document.execCommand('copy')
|
||||
}
|
||||
} catch {
|
||||
ok = false
|
||||
}
|
||||
document.body.removeChild(ta)
|
||||
if (ta.parentNode) ta.parentNode.removeChild(ta)
|
||||
if (ok) return true
|
||||
|
||||
// 2) 异步回退:Clipboard API(仅 HTTPS/localhost;注意此时手势可能已丢失,仅作兜底)
|
||||
|
||||
@@ -868,7 +868,7 @@ function todayStatusLabel(s: string) {
|
||||
<th class="px-4 py-2.5 font-medium w-[130px]">用户</th>
|
||||
<th class="px-4 py-2.5 font-medium w-[64px]">类型</th>
|
||||
<th class="px-4 py-2.5 font-medium">任务标题</th>
|
||||
<th class="px-4 py-2.5 font-medium w-[60px]">状态</th>
|
||||
<th class="px-4 py-2.5 font-medium w-[72px]">状态</th>
|
||||
<th class="px-4 py-2.5 font-medium">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -891,20 +891,22 @@ function todayStatusLabel(s: string) {
|
||||
<td class="px-4 py-2.5 truncate text-gray-700" :title="log.realTitle || log.title">
|
||||
{{ log.realTitle || log.title || '--' }}
|
||||
</td>
|
||||
<td class="px-4 py-2.5 whitespace-nowrap">
|
||||
<span
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium inline-block"
|
||||
:class="log.isSuccess ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
||||
>
|
||||
{{ log.isSuccess ? '成功' : '失败' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="log.deleted"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium inline-block ml-1 bg-gray-100 text-gray-400 border border-gray-200"
|
||||
title="用户已在客户端删除该日志,后台统计仍计入"
|
||||
>
|
||||
已删除
|
||||
</span>
|
||||
<td class="px-4 py-2.5">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium inline-block"
|
||||
:class="log.isSuccess ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
||||
>
|
||||
{{ log.isSuccess ? '成功' : '失败' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="log.deleted"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium inline-block bg-gray-100 text-gray-400 border border-gray-200"
|
||||
title="用户已在客户端删除该日志,后台统计仍计入"
|
||||
>
|
||||
已删除
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<span v-if="log.errorMessage" class="text-xs text-red-500 break-words">{{ log.errorMessage }}</span>
|
||||
|
||||
Reference in New Issue
Block a user