@@ -1,21 +1,223 @@
< script setup >
import { onMounted , ref } from 'vue' ;
const passkeyList = ref ( [ ] ) ;
const isLoading = ref ( true ) ;
const editingId = ref ( null ) ;
const editingName = ref ( '' ) ;
// 模态窗口状态
const showModal = ref ( false ) ;
const modalType = ref ( '' ) ; // 'confirm' | 'alert'
const modalTitle = ref ( '' ) ;
const modalMessage = ref ( '' ) ;
const modalConfirmCallback = ref ( null ) ;
onMounted ( ( ) => {
GetPasskeyList ( ) ;
} )
function GetPasskeyList ( ) {
isLoading . value = true ;
const loginServerUrl = localStorage . getItem ( 'loginServerUrl' ) ;
fetch ( loginServerUrl + '/passkeylist' , {
headers : {
Authorization : ` Bearer ${ window . localStorage . getItem ( 'bearer' ) } `
}
} ) . then ( res => {
res . json ( ) . then ( ( data ) => {
passkeyList . value = data ;
} )
} ) . catch ( err => {
console . error ( '获取通行密钥列表失败:' , err ) ;
} ) . finally ( ( ) => {
isLoading . value = false ;
} )
}
function CreatePasskey ( ) {
const authWindow = window . open (
` ${ localStorage . getItem ( 'loginClientUrl' ) } ?passkey ` ,
'统一身份认证' ,
'height=700,width=1000,top=300,left=200,toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no'
) ;
const checkClosed = setInterval ( ( ) => {
if ( authWindow . closed ) {
clearInterval ( checkClosed ) ;
GetPasskeyList ( ) ;
}
} , 500 ) ;
}
function StartEditName ( id , currentName ) {
editingId . value = id ;
editingName . value = currentName ;
}
function CancelEdit ( ) {
editingId . value = null ;
editingName . value = '' ;
}
function SavePasskeyName ( id ) {
if ( ! editingName . value . trim ( ) ) {
ShowAlert ( '提示' , '名称不能为空' ) ;
return ;
}
const loginServerUrl = localStorage . getItem ( 'loginServerUrl' ) ;
const formData = new FormData ( ) ;
formData . append ( 'id' , id ) ;
formData . append ( 'name' , editingName . value . trim ( ) ) ;
fetch ( loginServerUrl + '/changepasskeyname' , {
method : 'POST' ,
headers : {
Authorization : ` Bearer ${ window . localStorage . getItem ( 'bearer' ) } `
} ,
body : formData
} ) . then ( res => {
if ( res . ok ) {
const item = passkeyList . value . find ( p => p . id === id ) ;
if ( item ) {
item . name = editingName . value . trim ( ) ;
}
CancelEdit ( ) ;
} else {
ShowAlert ( '错误' , '修改名称失败,请重试' ) ;
}
} ) . catch ( ( ) => {
ShowAlert ( '错误' , '网络错误,请重试' ) ;
} )
}
function ShowConfirm ( title , message , callback ) {
modalType . value = 'confirm' ;
modalTitle . value = title ;
modalMessage . value = message ;
modalConfirmCallback . value = callback ;
showModal . value = true ;
}
function ShowAlert ( title , message ) {
modalType . value = 'alert' ;
modalTitle . value = title ;
modalMessage . value = message ;
modalConfirmCallback . value = null ;
showModal . value = true ;
}
function HandleConfirm ( ) {
if ( modalConfirmCallback . value ) {
modalConfirmCallback . value ( ) ;
}
CloseModal ( ) ;
}
function CloseModal ( ) {
showModal . value = false ;
modalConfirmCallback . value = null ;
}
function DeletePasskey ( id , name ) {
ShowConfirm ( '确认删除' , ` 确认删除通行密钥" ${ name } "吗? ` , ( ) => {
const loginServerUrl = localStorage . getItem ( 'loginServerUrl' ) ;
const formData = new FormData ( ) ;
formData . append ( 'id' , id ) ;
fetch ( loginServerUrl + '/deletepasskey' , {
method : 'POST' ,
headers : {
Authorization : ` Bearer ${ window . localStorage . getItem ( 'bearer' ) } `
} ,
body : formData
} ) . then ( res => {
if ( res . ok ) {
passkeyList . value = passkeyList . value . filter ( p => p . id !== id ) ;
ShowAlert ( '删除成功' , '请自行前往通行密钥的提供程序删除本地密钥。' ) ;
} else {
ShowAlert ( '删除失败' , '删除失败,请重试' ) ;
}
} ) . catch ( ( ) => {
ShowAlert ( '网络错误' , '网络错误,请重试' ) ;
} )
} ) ;
}
function formatDate ( dateStr ) {
if ( ! dateStr ) return '' ;
const date = new Date ( dateStr ) ;
return date . toLocaleString ( 'zh-CN' ) ;
}
< / script >
< template >
< div class = "passkey-page" >
< div class = "page-header" >
< h1 class = "page-title" > 通行密钥 < / h1 >
< p class = "page-subtitle" > 管理您的无密码登录方式 < / p >
< / div >
< div class = "action-bar" v-if = "passkeyList.length > 0" >
< button class = "btn btn-primary" @click ="CreatePasskey()" >
< span class = "btn-icon" > + < / span >
创建通行密钥
< / button >
< / div >
< div class = "card" >
< div class = "card-header" >
< h3 class = "card-title" > 我的通行密钥 < / h3 >
< span class = "badge" v-if = "passkeyList.length > 0" > {{ passkeyList.length }} < / span >
< / div >
< div class = "card-body" >
< div class = "empty -state" >
< div class = "empty-icon " > 🔑 < / div >
< h3 class = "empty-title" > 暂不支持通行密钥 < / h3 >
< p class = "empty-desc" > 该功能正在开发中 , 敬请期待 。 届时您将可以使用生物识别或安全密钥进行无密码登录 。 < / p >
< div class = "loading -state" v-if = "isLoading" >
< div class = "spinner " > < / div >
< span > 加载中 ... < / span >
< / div >
< div class = "empty-state" v-else-if = "passkeyList.length === 0" >
< div class = "empty-icon" > 🔑 < / div >
< h3 class = "empty-title" > 您还没有创建通行密钥 < / h3 >
< p class = "empty-desc" > 点击下方按钮创建您的第一个通行密钥 , 体验无密码登录的便捷 < / p >
< button class = "btn btn-primary" @click ="CreatePasskey()" >
< span class = "btn-icon" > + < / span >
创建通行密钥
< / button >
< / div >
< ul class = "passkey-list" v-else >
< li class = "passkey-item" v-for = "item in passkeyList" :key="item.id" >
< div class = "passkey-icon" > 🔐 < / div >
< div class = "passkey-info" >
< div class = "passkey-name" v-if = "editingId !== item.id" > {{ item.name }} < / div >
< div class = "passkey-edit" v-else >
< input
type = "text"
v-model = "editingName"
class = "edit-input"
@keyup.enter ="SavePasskeyName(item.id)"
@keyup.esc ="CancelEdit"
autofocus
/ >
< button class = "btn btn-sm btn-primary" @click ="SavePasskeyName(item.id)" > 保存 < / button >
< button class = "btn btn-sm btn-secondary" @click ="CancelEdit" > 取消 < / button >
< / div >
< div class = "passkey-meta" >
< span class = "passkey-date" v-if = "item.createdAt" > 创建于 {{ formatDate ( item.createdAt ) }} < / span >
< / div >
< / div >
< div class = "passkey-actions" v-if = "editingId !== item.id" >
< button class = "btn btn-sm btn-outline" @click ="StartEditName(item.id, item.name)" > 重命名 < / button >
< button class = "btn btn-sm btn-danger" @click ="DeletePasskey(item.id, item.name)" > 删除 < / button >
< / div >
< / li >
< / ul >
< / div >
< / div >
< div class = "card" style = "margin-top: 24px; " >
< div class = "card info-card " >
< div class = "card-header" >
< h3 class = "card-title" > 关于通行密钥 < / h3 >
< / div >
@@ -25,10 +227,10 @@
< div class = "feature-icon" > 🔐 < / div >
< div class = "feature-content" >
< div class = "feature-title" > 更安全 < / div >
< div class = "feature-desc" > 无需密码 , 使用生物识别或硬件密钥验证身份 < / div >
< div class = "feature-desc" > 无需密码 , 使用生物识别或硬件密钥验证身份 , 防止钓鱼攻击 < / div >
< / div >
< / div >
< div class = "feature-item" >
< div class = "feature-icon" > ⚡ < / div >
< div class = "feature-content" >
@@ -36,17 +238,34 @@
< div class = "feature-desc" > 一键登录 , 无需记忆复杂密码 < / div >
< / div >
< / div >
< div class = "feature-item" >
< div class = "feature-icon" > 🌐 < / div >
< div class = "feature-content" >
< div class = "feature-title" > 跨平台 < / div >
< div class = "feature-desc" > 支持多种设备和浏览器 < / div >
< div class = "feature-desc" > 支持多种设备和浏览器 , 随时随地使用 < / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- 模态窗口 -- >
< div class = "modal-overlay" v-if = "showModal" @click.self="CloseModal" >
< div class = "modal" >
< div class = "modal-header" >
< h3 class = "modal-title" > { { modalTitle } } < / h3 >
< button class = "modal-close" @click ="CloseModal" > × < / button >
< / div >
< div class = "modal-body" >
< p > { { modalMessage } } < / p >
< / div >
< div class = "modal-footer" >
< button v-if = "modalType === 'confirm'" class="btn btn-secondary" @click="CloseModal" > 取消 < / button >
< button class = "btn btn-primary" @click ="HandleConfirm" > 确定 < / button >
< / div >
< / div >
< / div >
< / div >
< / template >
@@ -55,10 +274,194 @@
max - width : 800 px ;
}
. page - header {
margin - bottom : 24 px ;
}
. action - bar {
margin - bottom : 20 px ;
}
. btn {
display : inline - flex ;
align - items : center ;
gap : 8 px ;
padding : 10 px 20 px ;
border : none ;
border - radius : var ( -- radius ) ;
font - size : 14 px ;
font - weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ;
}
. btn - primary {
background : linear - gradient ( 135 deg , var ( -- primary - color ) , var ( -- secondary - color ) ) ;
color : white ;
box - shadow : 0 2 px 4 px rgba ( 79 , 70 , 229 , 0.3 ) ;
}
. btn - primary : hover {
transform : translateY ( - 1 px ) ;
box - shadow : 0 4 px 8 px rgba ( 79 , 70 , 229 , 0.4 ) ;
}
. btn - secondary {
background : var ( -- background ) ;
color : var ( -- text - primary ) ;
border : 1 px solid var ( -- border - color ) ;
}
. btn - outline {
background : transparent ;
color : var ( -- primary - color ) ;
border : 1 px solid var ( -- primary - color ) ;
}
. btn - outline : hover {
background : var ( -- primary - color ) ;
color : white ;
}
. btn - danger {
background : transparent ;
color : var ( -- error - color ) ;
border : 1 px solid var ( -- error - color ) ;
}
. btn - danger : hover {
background : var ( -- error - color ) ;
color : white ;
}
. btn - sm {
padding : 6 px 12 px ;
font - size : 13 px ;
}
. btn - icon {
font - size : 18 px ;
font - weight : bold ;
}
. loading - state {
display : flex ;
align - items : center ;
justify - content : center ;
gap : 12 px ;
padding : 40 px ;
color : var ( -- text - secondary ) ;
}
. spinner {
width : 20 px ;
height : 20 px ;
border : 2 px solid var ( -- border - color ) ;
border - top - color : var ( -- primary - color ) ;
border - radius : 50 % ;
animation : spin 0.8 s linear infinite ;
}
@ keyframes spin {
to { transform : rotate ( 360 deg ) ; }
}
. empty - state {
text - align : center ;
padding : 40 px 20 px ;
}
. empty - title {
margin - bottom : 8 px ;
}
. empty - desc {
margin - bottom : 20 px ;
}
. passkey - list {
list - style : none ;
padding : 0 ;
margin : 0 ;
}
. passkey - item {
display : flex ;
align - items : center ;
gap : 16 px ;
padding : 16 px ;
border - radius : var ( -- radius ) ;
background : var ( -- background ) ;
margin - bottom : 12 px ;
}
. passkey - item : last - child {
margin - bottom : 0 ;
}
. passkey - icon {
font - size : 24 px ;
flex - shrink : 0 ;
}
. passkey - info {
flex : 1 ;
min - width : 0 ;
}
. passkey - name {
font - size : 15 px ;
font - weight : 500 ;
color : var ( -- text - primary ) ;
margin - bottom : 4 px ;
}
. passkey - edit {
display : flex ;
align - items : center ;
gap : 8 px ;
margin - bottom : 4 px ;
}
. edit - input {
flex : 1 ;
padding : 6 px 10 px ;
border : 1 px solid var ( -- border - color ) ;
border - radius : var ( -- radius ) ;
font - size : 14 px ;
background : var ( -- card - bg ) ;
color : var ( -- text - primary ) ;
}
. edit - input : focus {
outline : none ;
border - color : var ( -- primary - color ) ;
}
. passkey - meta {
display : flex ;
gap : 12 px ;
}
. passkey - date {
font - size : 12 px ;
color : var ( -- text - secondary ) ;
}
. passkey - actions {
display : flex ;
gap : 8 px ;
flex - shrink : 0 ;
}
. info - card {
margin - top : 24 px ;
}
. feature - list {
display : flex ;
flex - direction : column ;
gap : 20 px ;
gap : 16 px ;
}
. feature - item {
@@ -88,4 +491,73 @@
font - size : 14 px ;
color : var ( -- text - secondary ) ;
}
< / style >
/* 模态窗口样式 */
. modal - overlay {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
background : rgba ( 0 , 0 , 0 , 0.5 ) ;
display : flex ;
align - items : center ;
justify - content : center ;
z - index : 1000 ;
}
. modal {
background : var ( -- card - bg ) ;
border - radius : var ( -- radius - lg ) ;
width : 400 px ;
max - width : 90 % ;
box - shadow : var ( -- shadow - lg ) ;
}
. modal - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 20 px 24 px ;
border - bottom : 1 px solid var ( -- border - color ) ;
}
. modal - title {
font - size : 18 px ;
font - weight : 600 ;
color : var ( -- text - primary ) ;
margin : 0 ;
}
. modal - close {
background : none ;
border : none ;
font - size : 24 px ;
color : var ( -- text - secondary ) ;
cursor : pointer ;
padding : 0 ;
line - height : 1 ;
}
. modal - close : hover {
color : var ( -- text - primary ) ;
}
. modal - body {
padding : 24 px ;
}
. modal - body p {
margin : 0 ;
color : var ( -- text - primary ) ;
line - height : 1.6 ;
}
. modal - footer {
display : flex ;
justify - content : flex - end ;
gap : 12 px ;
padding : 16 px 24 px ;
border - top : 1 px solid var ( -- border - color ) ;
}
< / style >