fix: 校正一次性密码展示与 CSV 下载

This commit is contained in:
Nixevol
2026-09-30 16:22:49 +08:00
parent ed788fbb74
commit 15802bf706
9 changed files with 270 additions and 35 deletions
@@ -0,0 +1,73 @@
import { config, mount, flushPromises } from "@vue/test-utils";
import { describe, expect, it, vi, afterEach } from "vitest";
import { NConfigProvider, NMessageProvider, zhCN, dateZhCN } from "naive-ui";
import { defineComponent, h } from "vue";
import SecretOnceAlert from "./SecretOnceAlert.vue";
vi.mock("@/utils/notify", () => ({
message: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
},
}));
import { message } from "@/utils/notify";
config.global.stubs = { teleport: true };
function wrap() {
return defineComponent({
setup() {
return () =>
h(NConfigProvider, { locale: zhCN, dateLocale: dateZhCN, size: "small" }, {
default: () =>
h(NMessageProvider, null, {
default: () =>
h(SecretOnceAlert, {
title: "令牌只显示一次",
secret: "nxm_abc",
filename: "api-token.txt",
}),
}),
});
},
});
}
describe("SecretOnceAlert 复制", () => {
afterEach(() => {
vi.mocked(message.success).mockClear();
vi.mocked(message.error).mockClear();
});
it("不支持 clipboard 时回退 execCommand 并提示成功", async () => {
Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
Object.defineProperty(document, "execCommand", { value: vi.fn(() => true), configurable: true });
const w = mount(wrap(), { attachTo: document.body });
await flushPromises();
await w.find('[data-testid="secret-copy"]').trigger("click");
await flushPromises();
expect(document.execCommand).toHaveBeenCalledWith("copy");
expect(message.success).toHaveBeenCalledWith("已复制");
w.unmount();
});
it("writeText 失败时提示错误", async () => {
Object.defineProperty(navigator, "clipboard", {
value: { writeText: vi.fn().mockRejectedValue(new Error("denied")) },
configurable: true,
});
Object.defineProperty(document, "execCommand", { value: vi.fn(() => false), configurable: true });
const w = mount(wrap(), { attachTo: document.body });
await flushPromises();
await w.find('[data-testid="secret-copy"]').trigger("click");
await flushPromises();
expect(message.error).toHaveBeenCalledWith("复制失败,请手动选择文本");
w.unmount();
});
});
+49 -18
View File
@@ -1,40 +1,71 @@
<script setup lang="ts">
import { ref } from "vue";
import { NAlert, NButton, NSpace, NText } from "naive-ui";
import { downloadText } from "@/utils/csv";
import { message } from "@/utils/notify";
const props = defineProps<{
title: string;
secret: string;
filename?: string;
}>();
const props = withDefaults(
defineProps<{
title: string;
secret: string;
filename?: string;
mime?: string;
}>(),
{
filename: "secret.txt",
mime: "text/plain;charset=utf-8",
},
);
const emit = defineEmits<{
dismiss: [];
}>();
function copy() {
void navigator.clipboard?.writeText(props.secret);
const secretEl = ref<HTMLElement | null>(null);
async function copy() {
try {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(props.secret);
message.success("已复制");
return;
}
} catch {
/* 回退选中文本 */
}
const el = secretEl.value;
if (el) {
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(el);
sel?.removeAllRanges();
sel?.addRange(range);
}
try {
if (document.execCommand("copy")) {
message.success("已复制");
return;
}
} catch {
/* ignore */
}
message.error("复制失败,请手动选择文本");
}
function download() {
const blob = new Blob([props.secret], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = props.filename ?? "secret.txt";
a.click();
URL.revokeObjectURL(url);
downloadText(props.filename, props.secret, props.mime);
}
</script>
<template>
<n-alert type="warning" :title="title" style="margin-bottom: 12px">
<n-space vertical :size="8">
<n-text code>{{ secret }}</n-text>
<span ref="secretEl" data-testid="secret-text"><n-text code>{{ secret }}</n-text></span>
<n-text depth="3">关闭后将无法再次查看,请立即复制或下载。</n-text>
<n-space>
<n-button size="small" @click="copy">复制</n-button>
<n-button size="small" @click="download">下载</n-button>
<n-button size="small" secondary @click="emit('dismiss')">我已保存</n-button>
<n-button size="small" data-testid="secret-copy" @click="copy">复制</n-button>
<n-button size="small" data-testid="secret-download" @click="download">下载</n-button>
<n-button size="small" secondary data-testid="secret-dismiss" @click="emit('dismiss')">我已保存</n-button>
</n-space>
</n-space>
</n-alert>