feat: add follow unsubscribe integration

This commit is contained in:
Tim
2025-07-10 16:58:41 +08:00
parent 5ebda0fb1d
commit bef539fcea
3 changed files with 93 additions and 14 deletions

View File

@@ -12,11 +12,19 @@
<div class="profile-page-header-user-info"> <div class="profile-page-header-user-info">
<div class="profile-page-header-user-info-name">{{ user.username }}</div> <div class="profile-page-header-user-info-name">{{ user.username }}</div>
<div class="profile-page-header-user-info-description">{{ user.introduction }}</div> <div class="profile-page-header-user-info-description">{{ user.introduction }}</div>
<div class="profile-page-header-subscribe-button"> <div
v-if="!isMine && !subscribed"
class="profile-page-header-subscribe-button"
@click="subscribeUser"
>
<i class="fas fa-user-plus"></i> <i class="fas fa-user-plus"></i>
关注 关注
</div> </div>
<div class="profile-page-header-unsubscribe-button"> <div
v-if="!isMine && subscribed"
class="profile-page-header-unsubscribe-button"
@click="unsubscribeUser"
>
<i class="fas fa-user-minus"></i> <i class="fas fa-user-minus"></i>
取消关注 取消关注
</div> </div>
@@ -203,9 +211,10 @@
</template> </template>
<script> <script>
import { ref, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { API_BASE_URL } from '../main' import { API_BASE_URL, toast } from '../main'
import { getToken, authState } from '../utils/auth'
import BaseTimeline from '../components/BaseTimeline.vue' import BaseTimeline from '../components/BaseTimeline.vue'
import UserList from '../components/UserList.vue' import UserList from '../components/UserList.vue'
import { stripMarkdown } from '../utils/markdown' import { stripMarkdown } from '../utils/markdown'
@@ -226,19 +235,28 @@ export default {
const timelineItems = ref([]) const timelineItems = ref([])
const followers = ref([]) const followers = ref([])
const followings = ref([]) const followings = ref([])
const subscribed = ref(false)
const isLoading = ref(true) const isLoading = ref(true)
const tabLoading = ref(false) const tabLoading = ref(false)
const selectedTab = ref('summary') const selectedTab = ref('summary')
const followTab = ref('followers') const followTab = ref('followers')
const isMine = computed(() => authState.username === username)
const formatDate = (d) => { const formatDate = (d) => {
if (!d) return '' if (!d) return ''
return TimeManager.format(d) return TimeManager.format(d)
} }
const fetchUser = async () => { const fetchUser = async () => {
const res = await fetch(`${API_BASE_URL}/api/users/${username}`) const token = getToken()
if (res.ok) user.value = await res.json() const headers = token ? { Authorization: `Bearer ${token}` } : {}
const res = await fetch(`${API_BASE_URL}/api/users/${username}`, { headers })
if (res.ok) {
const data = await res.json()
user.value = data
subscribed.value = !!data.subscribed
}
} }
const fetchSummary = async () => { const fetchSummary = async () => {
@@ -303,6 +321,42 @@ export default {
tabLoading.value = false tabLoading.value = false
} }
const subscribeUser = async () => {
const token = getToken()
if (!token) {
toast.error('请先登录')
return
}
const res = await fetch(`${API_BASE_URL}/api/subscriptions/users/${username}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) {
subscribed.value = true
toast.success('已关注')
} else {
toast.error('操作失败')
}
}
const unsubscribeUser = async () => {
const token = getToken()
if (!token) {
toast.error('请先登录')
return
}
const res = await fetch(`${API_BASE_URL}/api/subscriptions/users/${username}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) {
subscribed.value = false
toast.success('已取消关注')
} else {
toast.error('操作失败')
}
}
const init = async () => { const init = async () => {
try { try {
await fetchUser() await fetchUser()
@@ -330,6 +384,8 @@ export default {
timelineItems, timelineItems,
followers, followers,
followings, followings,
subscribed,
isMine,
isLoading, isLoading,
tabLoading, tabLoading,
selectedTab, selectedTab,
@@ -338,7 +394,9 @@ export default {
stripMarkdown, stripMarkdown,
loadTimeline, loadTimeline,
loadFollow, loadFollow,
loadSummary loadSummary,
subscribeUser,
unsubscribeUser
} }
} }
} }

View File

@@ -42,7 +42,7 @@ public class UserController {
@GetMapping("/me") @GetMapping("/me")
public ResponseEntity<UserDto> me(Authentication auth) { public ResponseEntity<UserDto> me(Authentication auth) {
User user = userService.findByUsername(auth.getName()).orElseThrow(); User user = userService.findByUsername(auth.getName()).orElseThrow();
return ResponseEntity.ok(toDto(user)); return ResponseEntity.ok(toDto(user, auth));
} }
@PostMapping("/me/avatar") @PostMapping("/me/avatar")
@@ -68,13 +68,14 @@ public class UserController {
public ResponseEntity<UserDto> updateProfile(@RequestBody UpdateProfileDto dto, public ResponseEntity<UserDto> updateProfile(@RequestBody UpdateProfileDto dto,
Authentication auth) { Authentication auth) {
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction()); User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
return ResponseEntity.ok(toDto(user)); return ResponseEntity.ok(toDto(user, auth));
} }
@GetMapping("/{identifier}") @GetMapping("/{identifier}")
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier) { public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
Authentication auth) {
User user = userService.findByIdentifier(identifier).orElseThrow(); User user = userService.findByIdentifier(identifier).orElseThrow();
return ResponseEntity.ok(toDto(user)); return ResponseEntity.ok(toDto(user, auth));
} }
@GetMapping("/{identifier}/posts") @GetMapping("/{identifier}/posts")
@@ -138,7 +139,8 @@ public class UserController {
@GetMapping("/{identifier}/all") @GetMapping("/{identifier}/all")
public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier, public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier,
@RequestParam(value = "postsLimit", required = false) Integer postsLimit, @RequestParam(value = "postsLimit", required = false) Integer postsLimit,
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit) { @RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,
Authentication auth) {
User user = userService.findByIdentifier(identifier).orElseThrow(); User user = userService.findByIdentifier(identifier).orElseThrow();
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit; int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit; int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
@@ -149,13 +151,13 @@ public class UserController {
.map(this::toCommentInfoDto) .map(this::toCommentInfoDto)
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
UserAggregateDto dto = new UserAggregateDto(); UserAggregateDto dto = new UserAggregateDto();
dto.setUser(toDto(user)); dto.setUser(toDto(user, auth));
dto.setPosts(posts); dto.setPosts(posts);
dto.setReplies(replies); dto.setReplies(replies);
return ResponseEntity.ok(dto); return ResponseEntity.ok(dto);
} }
private UserDto toDto(User user) { private UserDto toDto(User user, Authentication viewer) {
UserDto dto = new UserDto(); UserDto dto = new UserDto();
dto.setId(user.getId()); dto.setId(user.getId());
dto.setUsername(user.getUsername()); dto.setUsername(user.getUsername());
@@ -168,9 +170,18 @@ public class UserController {
dto.setCreatedAt(user.getCreatedAt()); dto.setCreatedAt(user.getCreatedAt());
dto.setLastPostTime(postService.getLastPostTime(user.getUsername())); dto.setLastPostTime(postService.getLastPostTime(user.getUsername()));
dto.setTotalViews(postService.getTotalViews(user.getUsername())); dto.setTotalViews(postService.getTotalViews(user.getUsername()));
if (viewer != null) {
dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername()));
} else {
dto.setSubscribed(false);
}
return dto; return dto;
} }
private UserDto toDto(User user) {
return toDto(user, null);
}
private PostMetaDto toMetaDto(com.openisle.model.Post post) { private PostMetaDto toMetaDto(com.openisle.model.Post post) {
PostMetaDto dto = new PostMetaDto(); PostMetaDto dto = new PostMetaDto();
dto.setId(post.getId()); dto.setId(post.getId());
@@ -219,6 +230,7 @@ public class UserController {
private java.time.LocalDateTime createdAt; private java.time.LocalDateTime createdAt;
private java.time.LocalDateTime lastPostTime; private java.time.LocalDateTime lastPostTime;
private long totalViews; private long totalViews;
private boolean subscribed;
} }
@Data @Data

View File

@@ -99,4 +99,13 @@ public class SubscriptionService {
User user = userRepo.findByUsername(username).orElseThrow(); User user = userRepo.findByUsername(username).orElseThrow();
return userSubRepo.countBySubscriber(user); return userSubRepo.countBySubscriber(user);
} }
public boolean isSubscribed(String subscriberName, String targetName) {
if (subscriberName == null || targetName == null || subscriberName.equals(targetName)) {
return false;
}
User subscriber = userRepo.findByUsername(subscriberName).orElseThrow();
User target = userRepo.findByUsername(targetName).orElseThrow();
return userSubRepo.findBySubscriberAndTarget(subscriber, target).isPresent();
}
} }