mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-16 09:00:47 +08:00
feat: implement settings page and config management
This commit is contained in:
@@ -1,13 +1,133 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
<h2>设置</h2>
|
<h2>设置</h2>
|
||||||
<p>这里是设置页面。</p>
|
<div class="profile-section">
|
||||||
|
<div class="avatar-row">
|
||||||
|
<img :src="avatar" class="avatar-preview" />
|
||||||
|
<input type="file" @change="onAvatarChange" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input v-model="username" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>自我介绍</label>
|
||||||
|
<textarea v-model="introduction" rows="3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="role === 'ADMIN'" class="admin-section">
|
||||||
|
<h3>管理员设置</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>发布规则</label>
|
||||||
|
<select v-model="publishMode">
|
||||||
|
<option value="DIRECT">直接发布</option>
|
||||||
|
<option value="REVIEW">审核后发布</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>密码强度</label>
|
||||||
|
<select v-model="passwordStrength">
|
||||||
|
<option value="LOW">低</option>
|
||||||
|
<option value="MEDIUM">中</option>
|
||||||
|
<option value="HIGH">高</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button @click="cancel">取消</button>
|
||||||
|
<button @click="save">保存</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { API_BASE_URL, toast } from '../main'
|
||||||
|
import { getToken, fetchCurrentUser } from '../utils/auth'
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingsPageView'
|
name: 'SettingsPageView',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
username: '',
|
||||||
|
introduction: '',
|
||||||
|
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]
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
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('保存成功')
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -15,4 +135,31 @@ export default {
|
|||||||
.settings-page {
|
.settings-page {
|
||||||
padding: 20px;
|
padding: 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;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.model.PasswordStrength;
|
||||||
|
import com.openisle.model.PublishMode;
|
||||||
|
import com.openisle.service.PasswordValidator;
|
||||||
|
import com.openisle.service.PostService;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/config")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminConfigController {
|
||||||
|
private final PostService postService;
|
||||||
|
private final PasswordValidator passwordValidator;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ConfigDto getConfig() {
|
||||||
|
ConfigDto dto = new ConfigDto();
|
||||||
|
dto.setPublishMode(postService.getPublishMode());
|
||||||
|
dto.setPasswordStrength(passwordValidator.getStrength());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
|
||||||
|
if (dto.getPublishMode() != null) {
|
||||||
|
postService.setPublishMode(dto.getPublishMode());
|
||||||
|
}
|
||||||
|
if (dto.getPasswordStrength() != null) {
|
||||||
|
passwordValidator.setStrength(dto.getPasswordStrength());
|
||||||
|
}
|
||||||
|
return getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ConfigDto {
|
||||||
|
private PublishMode publishMode;
|
||||||
|
private PasswordStrength passwordStrength;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,13 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(Map.of("url", url));
|
return ResponseEntity.ok(Map.of("url", url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me")
|
||||||
|
public ResponseEntity<UserDto> updateProfile(@RequestBody UpdateProfileDto dto,
|
||||||
|
Authentication auth) {
|
||||||
|
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
||||||
|
return ResponseEntity.ok(toDto(user));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{username}")
|
@GetMapping("/{username}")
|
||||||
public ResponseEntity<UserDto> getUser(@PathVariable String username) {
|
public ResponseEntity<UserDto> getUser(@PathVariable String username) {
|
||||||
User user = userService.findByUsername(username).orElseThrow();
|
User user = userService.findByUsername(username).orElseThrow();
|
||||||
@@ -128,6 +135,8 @@ public class UserController {
|
|||||||
dto.setUsername(user.getUsername());
|
dto.setUsername(user.getUsername());
|
||||||
dto.setEmail(user.getEmail());
|
dto.setEmail(user.getEmail());
|
||||||
dto.setAvatar(user.getAvatar());
|
dto.setAvatar(user.getAvatar());
|
||||||
|
dto.setRole(user.getRole().name());
|
||||||
|
dto.setIntroduction(user.getIntroduction());
|
||||||
dto.setFollowers(subscriptionService.countSubscribers(user.getUsername()));
|
dto.setFollowers(subscriptionService.countSubscribers(user.getUsername()));
|
||||||
dto.setFollowing(subscriptionService.countSubscribed(user.getUsername()));
|
dto.setFollowing(subscriptionService.countSubscribed(user.getUsername()));
|
||||||
return dto;
|
return dto;
|
||||||
@@ -158,6 +167,8 @@ public class UserController {
|
|||||||
private String username;
|
private String username;
|
||||||
private String email;
|
private String email;
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
private String role;
|
||||||
|
private String introduction;
|
||||||
private long followers;
|
private long followers;
|
||||||
private long following;
|
private long following;
|
||||||
}
|
}
|
||||||
@@ -179,6 +190,12 @@ public class UserController {
|
|||||||
private Long postId;
|
private Long postId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
private static class UpdateProfileDto {
|
||||||
|
private String username;
|
||||||
|
private String introduction;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
private static class UserAggregateDto {
|
private static class UserAggregateDto {
|
||||||
private UserDto user;
|
private UserDto user;
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ public class User {
|
|||||||
|
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String introduction;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Role role = Role.USER;
|
private Role role = Role.USER;
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PasswordValidator {
|
public class PasswordValidator {
|
||||||
private final PasswordStrength strength;
|
private PasswordStrength strength;
|
||||||
|
|
||||||
public PasswordValidator(@Value("${app.password.strength:LOW}") PasswordStrength strength) {
|
public PasswordValidator(@Value("${app.password.strength:LOW}") PasswordStrength strength) {
|
||||||
this.strength = strength;
|
this.strength = strength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PasswordStrength getStrength() {
|
||||||
|
return strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrength(PasswordStrength strength) {
|
||||||
|
this.strength = strength;
|
||||||
|
}
|
||||||
|
|
||||||
public void validate(String password) {
|
public void validate(String password) {
|
||||||
if (password == null || password.isEmpty()) {
|
if (password == null || password.isEmpty()) {
|
||||||
throw new FieldException("password", "Password cannot be empty");
|
throw new FieldException("password", "Password cannot be empty");
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class PostService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
private final TagRepository tagRepository;
|
private final TagRepository tagRepository;
|
||||||
private final PublishMode publishMode;
|
private PublishMode publishMode;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
private final SubscriptionService subscriptionService;
|
private final SubscriptionService subscriptionService;
|
||||||
|
|
||||||
@@ -45,6 +45,14 @@ public class PostService {
|
|||||||
this.publishMode = publishMode;
|
this.publishMode = publishMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PublishMode getPublishMode() {
|
||||||
|
return publishMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublishMode(PublishMode publishMode) {
|
||||||
|
this.publishMode = publishMode;
|
||||||
|
}
|
||||||
|
|
||||||
public Post createPost(String username,
|
public Post createPost(String username,
|
||||||
Long categoryId,
|
Long categoryId,
|
||||||
String title,
|
String title,
|
||||||
|
|||||||
@@ -94,4 +94,19 @@ public class UserService {
|
|||||||
user.setAvatar(avatarUrl);
|
user.setAvatar(avatarUrl);
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public User updateProfile(String currentUsername, String newUsername, String introduction) {
|
||||||
|
User user = userRepository.findByUsername(currentUsername)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||||
|
if (newUsername != null && !newUsername.equals(currentUsername)) {
|
||||||
|
userRepository.findByUsername(newUsername).ifPresent(u -> {
|
||||||
|
throw new FieldException("username", "User name already exists");
|
||||||
|
});
|
||||||
|
user.setUsername(newUsername);
|
||||||
|
}
|
||||||
|
if (introduction != null) {
|
||||||
|
user.setIntroduction(introduction);
|
||||||
|
}
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user