feat(PassKey): 增加通行密钥管理功能
- 添加通行密钥列表的加载、编辑和删除功能 - 实现通行密钥名称的在线编辑功能 - 增加删除前的确认提示,包含密钥名称信息 - 添加加载状态指示器和空状态界面 - 优化UI界面,增加操作按钮和样式美化 - 添加创建时间显示和格式化功能 - 增强错误处理和网络异常提示
This commit is contained in:
@@ -2,26 +2,29 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const passkeyList = ref([]);
|
||||
const isLoading = ref(true);
|
||||
const editingId = ref(null);
|
||||
const editingName = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
// 获取passkey列表
|
||||
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) => {
|
||||
data.forEach(element => {
|
||||
passkeyList.value.push(element);
|
||||
});
|
||||
}
|
||||
)
|
||||
res.json().then((data) => {
|
||||
passkeyList.value = data;
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error('获取通行密钥列表失败:', err);
|
||||
}).finally(() => {
|
||||
isLoading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,16 +32,31 @@ function CreatePasskey() {
|
||||
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'
|
||||
'height=700,width=1000,top=300,left=200,toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no'
|
||||
)
|
||||
}
|
||||
|
||||
function ChangePasskeyName(id, name) {
|
||||
function StartEditName(id, currentName) {
|
||||
editingId.value = id;
|
||||
editingName.value = currentName;
|
||||
}
|
||||
|
||||
function CancelEdit() {
|
||||
editingId.value = null;
|
||||
editingName.value = '';
|
||||
}
|
||||
|
||||
function SavePasskeyName(id) {
|
||||
if (!editingName.value.trim()) {
|
||||
alert('名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const loginServerUrl = localStorage.getItem('loginServerUrl');
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('name', name);
|
||||
|
||||
formData.append('name', editingName.value.trim());
|
||||
|
||||
fetch(loginServerUrl + '/changepasskeyname', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -46,20 +64,29 @@ function ChangePasskeyName(id, name) {
|
||||
},
|
||||
body: formData
|
||||
}).then(res => {
|
||||
location.reload();
|
||||
if (res.ok) {
|
||||
const item = passkeyList.value.find(p => p.id === id);
|
||||
if (item) {
|
||||
item.name = editingName.value.trim();
|
||||
}
|
||||
CancelEdit();
|
||||
} else {
|
||||
alert('修改名称失败,请重试');
|
||||
}
|
||||
}).catch(() => {
|
||||
alert('网络错误,请重试');
|
||||
})
|
||||
}
|
||||
|
||||
function DeletePasskey(id) {
|
||||
function DeletePasskey(id, name) {
|
||||
if (!confirm(`确认删除通行密钥"${name}"吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loginServerUrl = localStorage.getItem('loginServerUrl');
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
// 弹出确认框
|
||||
if (!confirm('确认删除该通行密钥吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(loginServerUrl + '/deletepasskey', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -67,11 +94,22 @@ function DeletePasskey(id) {
|
||||
},
|
||||
body: formData
|
||||
}).then(res => {
|
||||
// 引导用户删除本地密钥
|
||||
alert('请自行前往通行密钥的提供程序删除本地密钥。')
|
||||
location.reload();
|
||||
if (res.ok) {
|
||||
passkeyList.value = passkeyList.value.filter(p => p.id !== id);
|
||||
alert('请自行前往通行密钥的提供程序删除本地密钥。');
|
||||
} else {
|
||||
alert('删除失败,请重试');
|
||||
}
|
||||
}).catch(() => {
|
||||
alert('网络错误,请重试');
|
||||
})
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,28 +119,65 @@ function DeletePasskey(id) {
|
||||
<p class="page-subtitle">管理您的无密码登录方式</p>
|
||||
</div>
|
||||
|
||||
<button @click="CreatePasskey()">创建通行密钥</button>
|
||||
|
||||
<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" v-if="passkeyList.length === 0">
|
||||
<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> -->
|
||||
<p class="empty-desc">点击下方按钮创建您的第一个通行密钥,体验无密码登录的便捷</p>
|
||||
<button class="btn btn-primary" @click="CreatePasskey()">
|
||||
<span class="btn-icon">+</span>
|
||||
创建通行密钥
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<li :key="item" v-for="item in passkeyList">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.id }}</span>
|
||||
<span>{{ item.createdAt }}</span>
|
||||
<button>修改名称</button>
|
||||
<button @click="DeletePasskey(item.id)">删除</button>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
@@ -112,10 +187,10 @@ function DeletePasskey(id) {
|
||||
<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">
|
||||
@@ -123,12 +198,12 @@ function DeletePasskey(id) {
|
||||
<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>
|
||||
@@ -142,10 +217,194 @@ function DeletePasskey(id) {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(79, 70, 229, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: transparent;
|
||||
color: var(--error-color);
|
||||
border: 1px solid var(--error-color);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.passkey-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.passkey-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--background);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.passkey-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.passkey-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.passkey-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.passkey-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.passkey-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.passkey-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.passkey-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.passkey-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
@@ -175,4 +434,4 @@ function DeletePasskey(id) {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user