Files
OpenIsle/open-isle-cli/src/views/SettingsPageView.vue
2025-07-08 13:03:40 +08:00

245 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="settings-page">
<div class="settings-title">设置</div>
<div class="profile-section">
<div class="avatar-row">
<!-- label 充当点击区域内部隐藏 input -->
<label class="avatar-container">
<img :src="avatar" class="avatar-preview" />
<!-- 半透明蒙层hover 时出现 -->
<div class="avatar-overlay">更换头像</div>
<input type="file" class="avatar-input" accept="image/*" @change="onAvatarChange" />
</label>
</div>
<div class="form-row username-row">
<BaseInput icon="fas fa-user" v-model="username" @input="usernameError = ''" placeholder="用户名" />
<div class="username-description">用户名是你在社区的唯一标识</div>
<div v-if="usernameError" class="error-message">{{ usernameError }}</div>
</div>
<div class="form-row">
<label>自我介绍</label>
<BaseInput v-model="introduction" textarea rows="3" />
<div class="introduction-description">自我介绍会出现在你的个人主页可以简要介绍自己</div>
</div>
</div>
<div v-if="role === 'ADMIN'" class="admin-section">
<h3>管理员设置</h3>
<div class="form-row">
<label>发布规则</label>
<Dropdown v-model="publishMode" :fetch-options="fetchPublishModes" />
</div>
<div class="form-row">
<label>密码强度</label>
<Dropdown v-model="passwordStrength" :fetch-options="fetchPasswordStrengths" />
</div>
</div>
<div class="buttons">
<button @click="save">保存</button>
</div>
</div>
</template>
<script>
import { API_BASE_URL, toast } from '../main'
import { getToken, fetchCurrentUser } from '../utils/auth'
import BaseInput from '../components/BaseInput.vue'
import Dropdown from '../components/Dropdown.vue'
export default {
name: 'SettingsPageView',
components: { BaseInput, Dropdown },
data() {
return {
username: '',
introduction: '',
usernameError: '',
avatar: '',
avatarFile: null,
role: '',
publishMode: 'DIRECT',
passwordStrength: 'LOW'
}
},
async mounted() {
const user = await fetchCurrentUser()
if (user) {
this.username = user.username
this.introduction = user.introduction || ''
this.avatar = user.avatar
this.role = user.role
if (this.role === 'ADMIN') {
this.loadAdminConfig()
}
}
},
methods: {
onAvatarChange(e) {
this.avatarFile = e.target.files[0]
},
fetchPublishModes() {
return Promise.resolve([
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
{ id: 'REVIEW', name: '审核后发布', icon: 'fas fa-search' }
])
},
fetchPasswordStrengths() {
return Promise.resolve([
{ id: 'LOW', name: '低', icon: 'fas fa-lock-open' },
{ id: 'MEDIUM', name: '中', icon: 'fas fa-lock' },
{ id: 'HIGH', name: '高', icon: 'fas fa-user-shield' }
])
},
async loadAdminConfig() {
try {
const token = getToken()
const res = await fetch(`${API_BASE_URL}/api/admin/config`, {
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) {
const data = await res.json()
this.publishMode = data.publishMode
this.passwordStrength = data.passwordStrength
}
} catch (e) {
// ignore
}
},
async save() {
const token = getToken()
this.usernameError = ''
if (!this.username) {
this.usernameError = '用户名不能为空'
} else if (this.username.length < 6) {
this.usernameError = '用户名至少6位'
}
if (this.usernameError) {
toast.error(this.usernameError)
return
}
if (this.avatarFile) {
const form = new FormData()
form.append('file', this.avatarFile)
const res = await fetch(`${API_BASE_URL}/api/users/me/avatar`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: form
})
const data = await res.json()
if (res.ok) {
this.avatar = data.url
} else {
toast.error(data.error || '上传失败')
return
}
}
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ username: this.username, introduction: this.introduction })
})
if (!res.ok) {
const data = await res.json()
toast.error(data.error || '保存失败')
return
}
if (this.role === 'ADMIN') {
await fetch(`${API_BASE_URL}/api/admin/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ publishMode: this.publishMode, passwordStrength: this.passwordStrength })
})
}
toast.success('保存成功')
},
}
}
</script>
<style scoped>
.settings-page {
background-color: var(--background-color);
padding: 40px;
height: calc(100vh - var(--header-height) - 80px);
}
.settings-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.avatar-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.avatar-preview {
width: 80px;
height: 80px;
border-radius: 40px;
object-fit: cover;
}
.form-row {
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
.admin-section {
margin-top: 30px;
padding-top: 10px;
}
.profile-section {
margin-bottom: 30px;
}
.buttons {
margin-top: 20px;
display: flex;
gap: 10px;
}
/* 容器:定位 + 光标 */
.avatar-container {
position: relative;
display: inline-block;
cursor: pointer;
}
/* 隐藏默认文件选择按钮 */
.avatar-input {
display: none;
}
/* 蒙层初始透明hover 时渐显 */
.avatar-overlay {
position: absolute;
inset: 0;
border-radius: 40px;
background: rgba(0, 0, 0, 0.4);
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
/* hover 触发 */
.avatar-container:hover .avatar-overlay {
opacity: 1;
}
.error-message {
color: red;
font-size: 14px;
width: calc(100% - 40px);
margin-top: -10px;
margin-bottom: 10px;
}
</style>