Skip to content

Commit 4aa1e65

Browse files
authored
feat: 优化token创建以及查看逻辑并修复Safari后台创建token后不显示token内容 (#1000)
* feat: 添加 Token 创建成功后的显示弹窗,支持复制 Token 到剪贴板 * feat: 添加 Token 可见性切换功能,支持复制 Token 到剪贴板
1 parent e7cd6cf commit 4aa1e65

File tree

1 file changed

+211
-25
lines changed

1 file changed

+211
-25
lines changed

src/views/setting/tabs/security.tsx

Lines changed: 211 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -202,31 +202,49 @@ const ApiToken = defineComponent(() => {
202202
fetchToken()
203203
})
204204
const newTokenDialogShow = ref(false)
205+
const tokenDisplayDialogShow = ref(false)
206+
const createdTokenInfo = ref<TokenModel | null>(null)
207+
const visibleTokens = ref<Set<string>>(new Set())
208+
205209
const newToken = async () => {
206-
const payload = {
207-
name: dataModel.name,
208-
expired: dataModel.expired
209-
? dataModel.expiredTime.toISOString()
210-
: undefined,
211-
}
210+
try {
211+
const payload = {
212+
name: dataModel.name,
213+
expired: dataModel.expired
214+
? dataModel.expiredTime.toISOString()
215+
: undefined,
216+
}
212217

213-
const response = (await RESTManager.api.auth.token.post({
214-
data: payload,
215-
})) as TokenModel
218+
const response = (await RESTManager.api.auth.token.post({
219+
data: payload,
220+
})) as TokenModel
216221

217-
await navigator.clipboard.writeText(response.token)
222+
// 尝试复制到剪贴板,但不阻塞后续流程
223+
try {
224+
await navigator.clipboard.writeText(response.token)
225+
} catch (clipboardError) {
226+
// Safari 或其他浏览器可能不支持或需要权限
227+
console.warn('复制到剪贴板失败:', clipboardError)
228+
}
218229

219-
newTokenDialogShow.value = false
220-
const n = defaultModel()
221-
for (const key in n) {
222-
dataModel[key] = n[key]
223-
}
224-
message.success(`生成成功,Token 已复制,${response.token}`)
225-
await fetchToken()
226-
// Backend bug.
227-
const index = tokens.value.findIndex((i) => i.name === payload.name)
228-
if (index !== -1) {
229-
tokens.value[index].token = response.token
230+
newTokenDialogShow.value = false
231+
const n = defaultModel()
232+
for (const key in n) {
233+
dataModel[key] = n[key]
234+
}
235+
236+
// 显示token详情弹窗
237+
createdTokenInfo.value = response
238+
tokenDisplayDialogShow.value = true
239+
240+
await fetchToken()
241+
// Backend bug.
242+
const index = tokens.value.findIndex((i) => i.name === payload.name)
243+
if (index !== -1) {
244+
tokens.value[index].token = response.token
245+
}
246+
} catch (error) {
247+
alert('创建 Token 失败,请重试')
230248
}
231249
}
232250

@@ -238,6 +256,53 @@ const ApiToken = defineComponent(() => {
238256
tokens.value.splice(index, 1)
239257
}
240258
}
259+
260+
const toggleTokenVisibility = async (tokenData: TokenModel) => {
261+
const tokenId = tokenData.id
262+
if (visibleTokens.value.has(tokenId)) {
263+
// 隐藏token
264+
visibleTokens.value.delete(tokenId)
265+
} else {
266+
// 显示token,需要从后端获取完整信息
267+
try {
268+
const response = await RESTManager.api.auth.token.get<TokenModel>({ params: { id: tokenId } })
269+
// 更新tokens数组中的token信息
270+
const index = tokens.value.findIndex((i) => i.id === tokenId)
271+
if (index !== -1) {
272+
tokens.value[index].token = response.token
273+
}
274+
visibleTokens.value.add(tokenId)
275+
} catch (error) {
276+
console.error('获取Token详情失败:', error)
277+
alert('获取Token详情失败,请重试')
278+
}
279+
}
280+
}
281+
282+
const copyToken = async (token: string) => {
283+
try {
284+
await navigator.clipboard.writeText(token)
285+
message.success('Token 已复制到剪贴板')
286+
} catch (error) {
287+
// Safari 兼容性处理:使用传统的复制方法
288+
const textArea = document.createElement('textarea')
289+
textArea.value = token
290+
textArea.style.position = 'fixed'
291+
textArea.style.left = '-9999px'
292+
document.body.appendChild(textArea)
293+
textArea.focus()
294+
textArea.select()
295+
try {
296+
document.execCommand('copy')
297+
message.success('Token 已复制到剪贴板')
298+
} catch (fallbackError) {
299+
console.warn('复制失败:', fallbackError)
300+
alert('复制失败,请手动复制Token')
301+
}
302+
document.body.removeChild(textArea)
303+
}
304+
}
305+
241306
const uiStore = useStoreRef(UIStore)
242307
return () => (
243308
<NLayoutContent class="!overflow-visible">
@@ -281,13 +346,106 @@ const ApiToken = defineComponent(() => {
281346
>
282347
取消
283348
</NButton>
284-
<NButton round type="primary" onClick={newToken}>
349+
<NButton
350+
round
351+
type="primary"
352+
disabled={!dataModel.name.trim()}
353+
onClick={newToken}
354+
>
285355
确定
286356
</NButton>
287357
</NSpace>
288358
</NCard>
289359
</NModal>
290360

361+
{/* Token 显示弹窗 */}
362+
<NModal
363+
transformOrigin="center"
364+
show={tokenDisplayDialogShow.value}
365+
onUpdateShow={(e) => void (tokenDisplayDialogShow.value = e)}
366+
>
367+
<NCard
368+
bordered={false}
369+
title="Token 创建成功"
370+
class="w-[600px] max-w-full"
371+
closable
372+
onClose={() => void (tokenDisplayDialogShow.value = false)}
373+
>
374+
<div class="space-y-4">
375+
<div>
376+
<NText depth={3} class="text-sm">Token 创建成功,请妥善保存以下信息:</NText>
377+
</div>
378+
379+
<div class="space-y-3">
380+
<div>
381+
<NText strong>Token 名称:</NText>
382+
<NText>{createdTokenInfo.value?.name}</NText>
383+
</div>
384+
385+
<div>
386+
<NText strong>Token:</NText>
387+
<div class="mt-2 p-3 bg-gray-50 dark:bg-gray-800 rounded border flex items-center gap-2">
388+
<NText code class="flex-1 break-all text-gray-900 dark:text-gray-100">{createdTokenInfo.value?.token}</NText>
389+
<NButton
390+
size="small"
391+
type="primary"
392+
onClick={async () => {
393+
if (createdTokenInfo.value?.token) {
394+
try {
395+
await navigator.clipboard.writeText(createdTokenInfo.value.token)
396+
message.success('Token 已复制到剪贴板')
397+
} catch (error) {
398+
// Safari 兼容性处理:使用传统的复制方法
399+
const textArea = document.createElement('textarea')
400+
textArea.value = createdTokenInfo.value.token
401+
textArea.style.position = 'fixed'
402+
textArea.style.left = '-9999px'
403+
document.body.appendChild(textArea)
404+
textArea.focus()
405+
textArea.select()
406+
try {
407+
document.execCommand('copy')
408+
message.success('Token 已复制到剪贴板')
409+
} catch (fallbackError) {
410+
console.warn('复制失败:', fallbackError)
411+
alert('复制失败,请手动复制Token')
412+
}
413+
document.body.removeChild(textArea)
414+
}
415+
}
416+
}}
417+
>
418+
复制
419+
</NButton>
420+
</div>
421+
</div>
422+
423+
{createdTokenInfo.value?.expired && (
424+
<div>
425+
<NText strong>过期时间:</NText>
426+
<NText>{createdTokenInfo.value.expired ? parseDate(createdTokenInfo.value.expired, 'yyyy 年 M 月 d 日 HH:mm:ss') : '永不过期'}</NText>
427+
</div>
428+
)}
429+
</div>
430+
431+
<div class="pt-2">
432+
<NText depth={3} class="text-sm">
433+
💡 建议将此 Token 保存在安全的地方,避免泄露给他人。
434+
</NText>
435+
</div>
436+
</div>
437+
438+
<div class="flex justify-end mt-6">
439+
<NButton
440+
type="primary"
441+
onClick={() => void (tokenDisplayDialogShow.value = false)}
442+
>
443+
确定
444+
</NButton>
445+
</div>
446+
</NCard>
447+
</NModal>
448+
291449
<NButton
292450
class="absolute right-0 top-[-3rem]"
293451
round
@@ -314,8 +472,26 @@ const ApiToken = defineComponent(() => {
314472
{
315473
key: 'token',
316474
title: 'Token',
317-
render({ token }) {
318-
return '*'.repeat(40)
475+
render(row) {
476+
const { token, id } = row
477+
const isVisible = visibleTokens.value.has(id)
478+
479+
if (isVisible && token && token !== '*'.repeat(40)) {
480+
// 显示真实token,可点击复制
481+
return (
482+
<NButton
483+
text
484+
type="primary"
485+
onClick={() => copyToken(token)}
486+
class="font-mono text-left max-w-[200px] truncate"
487+
>
488+
{token}
489+
</NButton>
490+
)
491+
} else {
492+
// 显示星号
493+
return '*'.repeat(40)
494+
}
319495
},
320496
},
321497
{
@@ -335,9 +511,19 @@ const ApiToken = defineComponent(() => {
335511
{
336512
title: '操作',
337513
key: 'id',
338-
render({ id, name }) {
514+
render(row) {
515+
const { id, name } = row
516+
const isVisible = visibleTokens.value.has(id)
517+
339518
return (
340519
<NSpace>
520+
<NButton
521+
text
522+
type="primary"
523+
onClick={() => toggleTokenVisibility(row)}
524+
>
525+
{isVisible ? '隐藏' : '查看'}
526+
</NButton>
341527
<NPopconfirm
342528
positiveText={'取消'}
343529
negativeText="删除"

0 commit comments

Comments
 (0)