@@ -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