197 lines
5.0 KiB
Vue
197 lines
5.0 KiB
Vue
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import api from '../api';
|
|
import { toast } from '../utils';
|
|
|
|
const router = useRouter();
|
|
const user = ref(JSON.parse(localStorage.getItem('user') || '{}'));
|
|
const editMode = ref(false);
|
|
const form = ref({ nickname: '', phone: '', building: '', bio: '' });
|
|
const pwdForm = ref({ oldPassword: '', newPassword: '', confirm: '' });
|
|
const showPwd = ref(false);
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const data = await api.get('/auth/me');
|
|
user.value = data;
|
|
localStorage.setItem('user', JSON.stringify(data));
|
|
form.value = { nickname: data.nickname, phone: data.phone, building: data.building, bio: data.bio };
|
|
} catch (e) {
|
|
toast(e.message);
|
|
}
|
|
});
|
|
|
|
async function saveProfile() {
|
|
try {
|
|
const data = await api.put('/auth/me', form.value);
|
|
user.value = data;
|
|
localStorage.setItem('user', JSON.stringify(data));
|
|
editMode.value = false;
|
|
toast('保存成功');
|
|
} catch (e) {
|
|
toast(e.message);
|
|
}
|
|
}
|
|
|
|
async function changePassword() {
|
|
const { oldPassword, newPassword, confirm } = pwdForm.value;
|
|
if (!oldPassword || !newPassword) return toast('请填写完整');
|
|
if (newPassword !== confirm) return toast('两次新密码不一致');
|
|
try {
|
|
await api.put('/auth/password', { oldPassword, newPassword });
|
|
toast('密码修改成功');
|
|
pwdForm.value = { oldPassword: '', newPassword: '', confirm: '' };
|
|
showPwd.value = false;
|
|
} catch (e) {
|
|
toast(e.message);
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('user');
|
|
router.push('/login');
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page">
|
|
<div class="card profile-card">
|
|
<div class="avatar big">{{ user.nickname?.charAt(0) }}</div>
|
|
<div class="info">
|
|
<div class="name">{{ user.nickname }}</div>
|
|
<div class="meta">@{{ user.username }}</div>
|
|
<div class="meta" v-if="user.building">📍 {{ user.building }}</div>
|
|
<div class="meta" v-if="user.bio">{{ user.bio }}</div>
|
|
</div>
|
|
<span v-if="user.role === 'admin'" class="badge">管理员</span>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<div class="section-title">
|
|
<h3>个人资料</h3>
|
|
<button class="btn btn-ghost btn-sm" @click="editMode = !editMode">
|
|
{{ editMode ? '取消' : '编辑' }}
|
|
</button>
|
|
</div>
|
|
<template v-if="editMode">
|
|
<div class="form">
|
|
<label>昵称</label>
|
|
<input v-model="form.nickname" />
|
|
<label>手机号</label>
|
|
<input v-model="form.phone" />
|
|
<label>楼栋/房号</label>
|
|
<input v-model="form.building" />
|
|
<label>个人简介</label>
|
|
<textarea v-model="form.bio" rows="3"></textarea>
|
|
<button class="btn" @click="saveProfile">保存</button>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="kv">
|
|
<div class="row"><span>昵称</span><span>{{ user.nickname }}</span></div>
|
|
<div class="row"><span>手机号</span><span>{{ user.phone || '未填写' }}</span></div>
|
|
<div class="row"><span>楼栋/房号</span><span>{{ user.building || '未填写' }}</span></div>
|
|
<div class="row"><span>个人简介</span><span>{{ user.bio || '未填写' }}</span></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<div class="section-title">
|
|
<h3>账号安全</h3>
|
|
<button class="btn btn-ghost btn-sm" @click="showPwd = !showPwd">
|
|
{{ showPwd ? '收起' : '修改密码' }}
|
|
</button>
|
|
</div>
|
|
<div v-if="showPwd" class="form">
|
|
<input v-model="pwdForm.oldPassword" type="password" placeholder="原密码" />
|
|
<input v-model="pwdForm.newPassword" type="password" placeholder="新密码(至少6位)" />
|
|
<input v-model="pwdForm.confirm" type="password" placeholder="确认新密码" />
|
|
<button class="btn" @click="changePassword">确认修改</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn btn-danger logout-btn" @click="logout">退出登录</button>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.profile-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.avatar.big {
|
|
width: 64px;
|
|
height: 64px;
|
|
font-size: 28px;
|
|
}
|
|
|
|
.info {
|
|
flex: 1;
|
|
}
|
|
|
|
.name {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.meta {
|
|
color: var(--text-light);
|
|
font-size: 13px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.section-title h3 {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.form label {
|
|
font-size: 13px;
|
|
color: var(--text-light);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.kv .row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.kv .row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.kv .row span:first-child {
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.logout-btn {
|
|
width: 100%;
|
|
margin-top: 8px;
|
|
}
|
|
</style>
|