Compare commits

...

2 Commits

Author SHA1 Message Date
79d035c852 feat(App): 添加页面加载状态显示
添加了isLoading响应式变量来控制页面加载状态,
在API请求过程中显示加载动画,并在请求完成(成功或失败)
后隐藏加载状态。同时优化了代码结构,将原来的
链式赋值改为单独赋值语句,提高了代码可读性。
2026-03-27 01:48:25 +08:00
e2a08ba5f1 feature:将修改用户名操作改为模态框 2026-03-27 01:42:02 +08:00
3 changed files with 307 additions and 68 deletions

View File

@@ -4,6 +4,7 @@ import { onMounted } from '@vue/runtime-core';
import Account from './components/Account.vue';
const isLogin = ref(false);
const isLoading = ref(true);
const userName = ref('');
const email = ref('');
const activeNav = ref('profile');
@@ -46,12 +47,18 @@ function BearerVerify() {
}).then(res => {
res.json().then(
(data) => {
;(isLogin.value = true), (userName.value = data.UserName), (email.value = data.Email)
isLogin.value = true;
userName.value = data.UserName;
email.value = data.Email;
isLoading.value = false;
}
)
}).catch(() => {
isLoading.value = false;
})
} else {
isLogin.value = false;
isLoading.value = false;
}
}
@@ -71,61 +78,69 @@ function setActiveNav(name) {
</script>
<template>
<div class="TopBar">
<div class="logo">
<div class="logo-icon">A</div>
<span>账户中心</span>
</div>
<div class="user-section">
<div class="user-info" v-if="isLogin">
<div class="user-avatar">{{ userName.charAt(0).toUpperCase() }}</div>
<span>{{ userName }}</span>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p class="loading-text">加载中...</p>
</div>
<template v-else>
<div class="TopBar">
<div class="logo">
<div class="logo-icon">A</div>
<span>账户中心</span>
</div>
<button v-if="!isLogin" class="login-btn" @click="Login()">
登录
</button>
<button v-else class="logout-btn" @click="Login()">
登出
</button>
</div>
</div>
<div class="Notice">
<span class="Notice-icon"></span>
<span>提示修改密码请登出后使用忘记密码功能</span>
</div>
<div v-if="isLogin" class="main-container">
<div class="LeftBar">
<div class="nav-title">导航菜单</div>
<div class="nav-list">
<RouterLink :to="{name: 'profile'}" @click="setActiveNav('profile')">
<div class="nav-item" :class="{ active: activeNav === 'profile' }">
<div class="nav-icon">👤</div>
<span>个人资料</span>
</div>
</RouterLink>
<RouterLink :to="{name: 'passkey'}" @click="setActiveNav('passkey')">
<div class="nav-item" :class="{ active: activeNav === 'passkey' }">
<div class="nav-icon">🔑</div>
<span>通行密钥</span>
</div>
</RouterLink>
<div class="user-section">
<div class="user-info" v-if="isLogin">
<div class="user-avatar">{{ userName.charAt(0).toUpperCase() }}</div>
<span>{{ userName }}</span>
</div>
<button v-if="!isLogin" class="login-btn" @click="Login()">
登录
</button>
<button v-else class="logout-btn" @click="Login()">
登出
</button>
</div>
</div>
<div class="content-area">
<RouterView :UserName="userName" :Email="email"/>
<div class="Notice">
<span class="Notice-icon"></span>
<span>提示修改密码请登出后使用忘记密码功能</span>
</div>
</div>
<div v-else class="login-prompt">
<div class="login-card">
<div class="login-icon">🔐</div>
<h2 class="login-title">欢迎使用账户中心</h2>
<p class="login-desc">请登录以管理您的账户信息和安全设置</p>
<button class="login-btn" @click="Login()">
立即登录
</button>
<div v-if="isLogin" class="main-container">
<div class="LeftBar">
<div class="nav-title">导航菜单</div>
<div class="nav-list">
<RouterLink :to="{name: 'profile'}" @click="setActiveNav('profile')">
<div class="nav-item" :class="{ active: activeNav === 'profile' }">
<div class="nav-icon">👤</div>
<span>个人资料</span>
</div>
</RouterLink>
<RouterLink :to="{name: 'passkey'}" @click="setActiveNav('passkey')">
<div class="nav-item" :class="{ active: activeNav === 'passkey' }">
<div class="nav-icon">🔑</div>
<span>通行密钥</span>
</div>
</RouterLink>
</div>
</div>
<div class="content-area">
<RouterView :UserName="userName" :Email="email"/>
</div>
</div>
</div>
<div v-else class="login-prompt">
<div class="login-card">
<div class="login-icon">🔐</div>
<h2 class="login-title">欢迎使用账户中心</h2>
<p class="login-desc">请登录以管理您的账户信息和安全设置</p>
<button class="login-btn" @click="Login()">
立即登录
</button>
</div>
</div>
</template>
</template>

View File

@@ -6,22 +6,43 @@ const props = defineProps({
Email: String,
})
const showModal = ref(false);
const newUserName = ref('');
const isSubmitting = ref(false);
function openModal() {
newUserName.value = props.UserName;
showModal.value = true;
}
function closeModal() {
showModal.value = false;
newUserName.value = '';
}
function ChangeUserName() {
let newUserName = prompt('请输入新的用户名');
if (newUserName !== null && newUserName !== props.UserName && newUserName.length > 0) {
const loginServerUrl = localStorage.getItem('loginServerUrl');
const formData = new FormData();
formData.append('name', newUserName);
fetch(loginServerUrl + '/changename', {
method: 'POST',
headers: {
Authorization: `Bearer ${window.localStorage.getItem('bearer')}`
},
body: formData
}).then(res => {
location.reload(true);
})
if (newUserName.value === null || newUserName.value === props.UserName || newUserName.value.length === 0) {
return;
}
isSubmitting.value = true;
const loginServerUrl = localStorage.getItem('loginServerUrl');
const formData = new FormData();
formData.append('name', newUserName.value);
fetch(loginServerUrl + '/changename', {
method: 'POST',
headers: {
Authorization: `Bearer ${window.localStorage.getItem('bearer')}`
},
body: formData
}).then(res => {
isSubmitting.value = false;
closeModal();
location.reload(true);
}).catch(() => {
isSubmitting.value = false;
})
}
</script>
@@ -41,7 +62,7 @@ function ChangeUserName() {
<label class="form-label">用户名</label>
<div class="form-row">
<div class="form-value">{{ props.UserName }}</div>
<button class="edit-btn" @click="ChangeUserName()">编辑</button>
<button class="edit-btn" @click="openModal()">编辑</button>
</div>
</div>
@@ -83,6 +104,38 @@ function ChangeUserName() {
</div>
</div>
</div>
<!-- 修改用户名模态窗 -->
<div class="modal-overlay" v-if="showModal" @click.self="closeModal()">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">修改用户名</h3>
<button class="modal-close" @click="closeModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">新用户名</label>
<input
type="text"
class="form-input"
v-model="newUserName"
placeholder="请输入新的用户名"
@keyup.enter="ChangeUserName()"
/>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeModal()">取消</button>
<button
class="btn btn-primary"
@click="ChangeUserName()"
:disabled="isSubmitting || newUserName === props.UserName || newUserName.length === 0"
>
{{ isSubmitting ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
@@ -132,4 +185,145 @@ function ChangeUserName() {
font-size: 13px;
color: var(--text-secondary);
}
/* 模态窗样式 */
.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;
animation: fadeIn 0.2s ease;
}
.modal {
background: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
width: 100%;
max-width: 420px;
margin: 16px;
animation: slideUp 0.2s ease;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.modal-close {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: var(--text-secondary);
transition: all 0.2s ease;
}
.modal-close:hover {
background: var(--background);
color: var(--text-primary);
}
.modal-body {
padding: 24px;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-size: 14px;
color: var(--text-primary);
background: var(--card-bg);
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
.form-input::placeholder {
color: var(--text-secondary);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid var(--border-color);
}
.btn {
padding: 10px 20px;
border-radius: var(--radius);
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-secondary {
background: var(--background);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: #fee2e2;
color: var(--error-color);
border-color: #fecaca;
}
.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:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(79, 70, 229, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -386,4 +386,34 @@ a {
.user-info {
display: none;
}
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
gap: 16px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
color: var(--text-secondary);
font-size: 14px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}