Compare commits

...

3 Commits

Author SHA1 Message Date
44c7a8c304 feat(Account): 添加路由支持并改进通行密钥管理界面
- 在App.vue中引入useRoute并根据当前路由设置默认导航项
- 在PassKey.vue中添加模态窗口组件用于显示确认和警告对话框
- 替换原生alert和confirm为自定义模态窗口
- 实现通行密
2026-03-28 01:09:59 +08:00
ddce6e6036 feat(PassKey): 增加通行密钥管理功能
- 添加通行密钥列表的加载、编辑和删除功能
- 实现通行密钥名称的在线编辑功能
- 增加删除前的确认提示,包含密钥名称信息
- 添加加载状态指示器和空状态界面
- 优化UI界面,增加操作按钮和样式美化
- 添加创建时间显示和格式化功能
- 增强错误处理和网络异常提示
2026-03-27 23:42:21 +08:00
cloudy2331
167b3348b1 feat(PassKey):提供了操作passkey的方法 2026-03-27 19:51:58 +08:00
2 changed files with 489 additions and 14 deletions

View File

@@ -1,18 +1,21 @@
<script setup>
import { ref } from '@vue/reactivity';
import { onMounted } from '@vue/runtime-core';
import { useRoute } from 'vue-router';
import Account from './components/Account.vue';
const route = useRoute();
const isLogin = ref(false);
const isLoading = ref(true);
const userName = ref('');
const email = ref('');
const activeNav = ref('profile');
const activeNav = ref(route.name || 'profile');
//初始化登录信息
var bearer = localStorage.getItem('bearer');
const loginClientUrl = 'https://www.cloudy233.top/UniAuth';
const loginServerUrl = 'https://www.cloudy233.top:6699';
localStorage.setItem('loginClientUrl', loginClientUrl);
localStorage.setItem('loginServerUrl', loginServerUrl);
onMounted(() => {

View File

@@ -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: 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 {
@@ -88,4 +491,73 @@
font-size: 14px;
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: 400px;
max-width: 90%;
box-shadow: var(--shadow-lg);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
line-height: 1;
}
.modal-close:hover {
color: var(--text-primary);
}
.modal-body {
padding: 24px;
}
.modal-body p {
margin: 0;
color: var(--text-primary);
line-height: 1.6;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid var(--border-color);
}
</style>