① 用户管理「当日执行」弹窗:状态列的「成功/失败」与「已删除」两个标签原在同一行
(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),
|
* 若先 await Clipboard API 被拒(移动端 WebView 常见 NotAllowedError),
|
||||||
* 手势上下文已丢失,后续 execCommand 会静默失败(返回 false 但剪贴板无内容)。
|
* 手势上下文已丢失,后续 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> {
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
// 1) 同步路径:隐藏 textarea + execCommand(用户手势内,兼容性最好)
|
// 1) 同步路径:隐藏 textarea + execCommand(用户手势内,兼容性最好)
|
||||||
const ta = document.createElement('textarea')
|
const ta = document.createElement('textarea')
|
||||||
@@ -16,17 +33,24 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|||||||
// font-size ≥ 16px 避免 iOS focus 时触发页面缩放
|
// 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.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', '')
|
ta.setAttribute('readonly', '')
|
||||||
document.body.appendChild(ta)
|
// 关键:挂到 dialog 容器内(若有弹窗打开),否则焦点会被 FocusScope 抢回导致为空复制
|
||||||
ta.focus()
|
const host: HTMLElement = findActiveDialogContainer() || document.body
|
||||||
ta.select()
|
host.appendChild(ta)
|
||||||
ta.setSelectionRange(0, text.length)
|
|
||||||
let ok = false
|
let ok = false
|
||||||
try {
|
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 {
|
} catch {
|
||||||
ok = false
|
ok = false
|
||||||
}
|
}
|
||||||
document.body.removeChild(ta)
|
if (ta.parentNode) ta.parentNode.removeChild(ta)
|
||||||
if (ok) return true
|
if (ok) return true
|
||||||
|
|
||||||
// 2) 异步回退:Clipboard API(仅 HTTPS/localhost;注意此时手势可能已丢失,仅作兜底)
|
// 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-[130px]">用户</th>
|
||||||
<th class="px-4 py-2.5 font-medium w-[64px]">类型</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">任务标题</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>
|
<th class="px-4 py-2.5 font-medium">说明</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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">
|
<td class="px-4 py-2.5 truncate text-gray-700" :title="log.realTitle || log.title">
|
||||||
{{ log.realTitle || log.title || '--' }}
|
{{ log.realTitle || log.title || '--' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 whitespace-nowrap">
|
<td class="px-4 py-2.5">
|
||||||
<span
|
<div class="flex flex-col items-start gap-1">
|
||||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium inline-block"
|
<span
|
||||||
:class="log.isSuccess ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
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>
|
{{ log.isSuccess ? '成功' : '失败' }}
|
||||||
<span
|
</span>
|
||||||
v-if="log.deleted"
|
<span
|
||||||
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"
|
v-if="log.deleted"
|
||||||
title="用户已在客户端删除该日志,后台统计仍计入"
|
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>
|
已删除
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5">
|
<td class="px-4 py-2.5">
|
||||||
<span v-if="log.errorMessage" class="text-xs text-red-500 break-words">{{ log.errorMessage }}</span>
|
<span v-if="log.errorMessage" class="text-xs text-red-500 break-words">{{ log.errorMessage }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user