mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 16:11:14 +08:00
Merge pull request #193 from nagisa77/codex/add-hot-tag-feature-to-profileview
Add tag timeline and summary features
This commit is contained in:
@@ -136,6 +136,27 @@
|
||||
<div class="summary-empty">暂无热门话题</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hot-tag">
|
||||
<div class="summary-title">TA创建的tag</div>
|
||||
<div class="summary-content" v-if="hotTags.length > 0">
|
||||
<BaseTimeline :items="hotTags">
|
||||
<template #item="{ item }">
|
||||
<span class="timeline-link" @click="gotoTag(item.tag)">
|
||||
{{ item.tag.name }}<span v-if="item.tag.count"> x{{ item.tag.count }}</span>
|
||||
</span>
|
||||
<div class="timeline-snippet" v-if="item.tag.description">
|
||||
{{ item.tag.description }}
|
||||
</div>
|
||||
<div class="timeline-date">
|
||||
{{ formatDate(item.tag.createdAt) }}
|
||||
</div>
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="summary-empty">暂无标签</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,6 +198,16 @@
|
||||
</router-link>
|
||||
<div class="timeline-date">{{ formatDate(item.createdAt) }}</div>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'tag'">
|
||||
创建了标签
|
||||
<span class="timeline-link" @click="gotoTag(item.tag)">
|
||||
{{ item.tag.name }}<span v-if="item.tag.count"> x{{ item.tag.count }}</span>
|
||||
</span>
|
||||
<div class="timeline-snippet" v-if="item.tag.description">
|
||||
{{ item.tag.description }}
|
||||
</div>
|
||||
<div class="timeline-date">{{ formatDate(item.createdAt) }}</div>
|
||||
</template>
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
</div>
|
||||
@@ -203,7 +234,7 @@
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, authState } from '../utils/auth'
|
||||
import BaseTimeline from '../components/BaseTimeline.vue'
|
||||
@@ -219,11 +250,13 @@ export default {
|
||||
components: { BaseTimeline, UserList, BasePlaceholder },
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const username = route.params.id
|
||||
|
||||
const user = ref({})
|
||||
const hotPosts = ref([])
|
||||
const hotReplies = ref([])
|
||||
const hotTags = ref([])
|
||||
const timelineItems = ref([])
|
||||
const followers = ref([])
|
||||
const followings = ref([])
|
||||
@@ -263,13 +296,21 @@ export default {
|
||||
const data = await repliesRes.json()
|
||||
hotReplies.value = data.map(c => ({ icon: 'fas fa-comment', comment: c }))
|
||||
}
|
||||
|
||||
const tagsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-tags`)
|
||||
if (tagsRes.ok) {
|
||||
const data = await tagsRes.json()
|
||||
hotTags.value = data.map(t => ({ icon: 'fas fa-tag', tag: t }))
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTimeline = async () => {
|
||||
const postsRes = await fetch(`${API_BASE_URL}/api/users/${username}/posts?limit=50`)
|
||||
const repliesRes = await fetch(`${API_BASE_URL}/api/users/${username}/replies?limit=50`)
|
||||
const tagsRes = await fetch(`${API_BASE_URL}/api/users/${username}/tags?limit=50`)
|
||||
const posts = postsRes.ok ? await postsRes.json() : []
|
||||
const replies = repliesRes.ok ? await repliesRes.json() : []
|
||||
const tags = tagsRes.ok ? await tagsRes.json() : []
|
||||
const mapped = [
|
||||
...posts.map(p => ({
|
||||
type: 'post',
|
||||
@@ -282,6 +323,12 @@ export default {
|
||||
icon: 'fas fa-comment',
|
||||
comment: r,
|
||||
createdAt: r.createdAt
|
||||
})),
|
||||
...tags.map(t => ({
|
||||
type: 'tag',
|
||||
icon: 'fas fa-tag',
|
||||
tag: t,
|
||||
createdAt: t.createdAt
|
||||
}))
|
||||
]
|
||||
mapped.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
@@ -349,6 +396,13 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const gotoTag = tag => {
|
||||
const value = encodeURIComponent(tag.id ?? tag.name)
|
||||
router.push({ path: '/', query: { tags: value } }).then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
await fetchUser()
|
||||
@@ -388,7 +442,9 @@ export default {
|
||||
loadFollow,
|
||||
loadSummary,
|
||||
subscribeUser,
|
||||
unsubscribeUser
|
||||
unsubscribeUser,
|
||||
gotoTag,
|
||||
hotTags
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,8 +625,9 @@ export default {
|
||||
}
|
||||
|
||||
.hot-reply,
|
||||
.hot-topic {
|
||||
width: 50%;
|
||||
.hot-topic,
|
||||
.hot-tag {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.profile-timeline {
|
||||
|
||||
@@ -30,7 +30,13 @@ public class TagController {
|
||||
approved = false;
|
||||
}
|
||||
}
|
||||
Tag tag = tagService.createTag(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon(), approved);
|
||||
Tag tag = tagService.createTag(
|
||||
req.getName(),
|
||||
req.getDescription(),
|
||||
req.getIcon(),
|
||||
req.getSmallIcon(),
|
||||
approved,
|
||||
auth != null ? auth.getName() : null);
|
||||
long count = postService.countPostsByTag(tag.getId());
|
||||
return toDto(tag, count);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public class UserController {
|
||||
private final PostService postService;
|
||||
private final CommentService commentService;
|
||||
private final ReactionService reactionService;
|
||||
private final TagService tagService;
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final PostReadService postReadService;
|
||||
private final UserVisitService userVisitService;
|
||||
@@ -38,6 +39,9 @@ public class UserController {
|
||||
@Value("${app.user.replies-limit:50}")
|
||||
private int defaultRepliesLimit;
|
||||
|
||||
@Value("${app.user.tags-limit:50}")
|
||||
private int defaultTagsLimit;
|
||||
|
||||
@Value("${app.snippet-length:50}")
|
||||
private int snippetLength;
|
||||
|
||||
@@ -122,6 +126,48 @@ public class UserController {
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/hot-tags")
|
||||
public java.util.List<TagInfoDto> hotTags(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : 10;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return tagService.getTagsByUser(user.getUsername()).stream()
|
||||
.map(t -> {
|
||||
TagInfoDto dto = new TagInfoDto();
|
||||
dto.setId(t.getId());
|
||||
dto.setName(t.getName());
|
||||
dto.setDescription(t.getDescription());
|
||||
dto.setIcon(t.getIcon());
|
||||
dto.setSmallIcon(t.getSmallIcon());
|
||||
dto.setCreatedAt(t.getCreatedAt());
|
||||
dto.setCount(postService.countPostsByTag(t.getId()));
|
||||
return dto;
|
||||
})
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.limit(l)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/tags")
|
||||
public java.util.List<TagInfoDto> userTags(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : defaultTagsLimit;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
|
||||
.map(t -> {
|
||||
TagInfoDto dto = new TagInfoDto();
|
||||
dto.setId(t.getId());
|
||||
dto.setName(t.getName());
|
||||
dto.setDescription(t.getDescription());
|
||||
dto.setIcon(t.getIcon());
|
||||
dto.setSmallIcon(t.getSmallIcon());
|
||||
dto.setCreatedAt(t.getCreatedAt());
|
||||
dto.setCount(postService.countPostsByTag(t.getId()));
|
||||
return dto;
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/following")
|
||||
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
@@ -262,6 +308,17 @@ public class UserController {
|
||||
private ParentCommentDto parentComment;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TagInfoDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
private java.time.LocalDateTime createdAt;
|
||||
private Long count;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ParentCommentDto {
|
||||
private Long id;
|
||||
|
||||
@@ -4,6 +4,10 @@ import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@@ -29,4 +33,13 @@ public class Tag {
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean approved = true;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false,
|
||||
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "creator_id")
|
||||
private User creator;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -10,4 +12,7 @@ public interface TagRepository extends JpaRepository<Tag, Long> {
|
||||
List<Tag> findByApproved(boolean approved);
|
||||
List<Tag> findByApprovedTrue();
|
||||
List<Tag> findByNameContainingIgnoreCaseAndApprovedTrue(String keyword);
|
||||
|
||||
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
|
||||
List<Tag> findByCreator(User creator);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.TagRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -12,8 +16,9 @@ import java.util.List;
|
||||
public class TagService {
|
||||
private final TagRepository tagRepository;
|
||||
private final TagValidator tagValidator;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon, boolean approved) {
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon, boolean approved, String creatorUsername) {
|
||||
tagValidator.validate(name);
|
||||
Tag tag = new Tag();
|
||||
tag.setName(name);
|
||||
@@ -21,11 +26,20 @@ public class TagService {
|
||||
tag.setIcon(icon);
|
||||
tag.setSmallIcon(smallIcon);
|
||||
tag.setApproved(approved);
|
||||
if (creatorUsername != null) {
|
||||
User creator = userRepository.findByUsername(creatorUsername)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
tag.setCreator(creator);
|
||||
}
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon, boolean approved) {
|
||||
return createTag(name, description, icon, smallIcon, approved, null);
|
||||
}
|
||||
|
||||
public Tag createTag(String name, String description, String icon, String smallIcon) {
|
||||
return createTag(name, description, icon, smallIcon, true);
|
||||
return createTag(name, description, icon, smallIcon, true, null);
|
||||
}
|
||||
|
||||
public Tag updateTag(Long id, String name, String description, String icon, String smallIcon) {
|
||||
@@ -77,4 +91,17 @@ public class TagService {
|
||||
}
|
||||
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
|
||||
}
|
||||
|
||||
public List<Tag> getRecentTagsByUser(String username, int limit) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
return tagRepository.findByCreatorOrderByCreatedAtDesc(user, pageable);
|
||||
}
|
||||
|
||||
public List<Tag> getTagsByUser(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
return tagRepository.findByCreator(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@@ -38,7 +39,7 @@ class TagControllerTest {
|
||||
t.setDescription("d");
|
||||
t.setIcon("i");
|
||||
t.setSmallIcon("s1");
|
||||
Mockito.when(tagService.createTag(eq("java"), eq("d"), eq("i"), eq("s1"))).thenReturn(t);
|
||||
Mockito.when(tagService.createTag(eq("java"), eq("d"), eq("i"), eq("s1"), eq(true), isNull())).thenReturn(t);
|
||||
Mockito.when(tagService.getTag(1L)).thenReturn(t);
|
||||
|
||||
mockMvc.perform(post("/api/tags")
|
||||
|
||||
Reference in New Issue
Block a user