mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-09 08:30:55 +08:00
Compare commits
17 Commits
codex/fix-
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47134cadc2 | ||
|
|
1f9ae8d057 | ||
|
|
da1ad73cf6 | ||
|
|
53c603f33a | ||
|
|
06f86f2b21 | ||
|
|
22693bfdd9 | ||
|
|
0058f20b1e | ||
|
|
304d941d68 | ||
|
|
3dbcd2ac4d | ||
|
|
2efe4e733a | ||
|
|
08239a16b8 | ||
|
|
cb49dc9b73 | ||
|
|
43d4c9be43 | ||
|
|
1dc13698ad | ||
|
|
d58432dcd9 | ||
|
|
e7ff73c7f9 | ||
|
|
4ee9532d5f |
@@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@@ -44,8 +45,11 @@ public class CategoryController {
|
||||
|
||||
@GetMapping
|
||||
public List<CategoryDto> list() {
|
||||
return categoryService.listCategories().stream()
|
||||
.map(c -> categoryMapper.toDto(c, postService.countPostsByCategory(c.getId())))
|
||||
List<Category> all = categoryService.listCategories();
|
||||
List<Long> ids = all.stream().map(Category::getId).toList();
|
||||
Map<Long, Long> postsCntByCategoryIds = postService.countPostsByCategoryIds(ids);
|
||||
return all.stream()
|
||||
.map(c -> categoryMapper.toDto(c, postsCntByCategoryIds.getOrDefault(c.getId(), 0L)))
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@@ -62,8 +63,11 @@ public class TagController {
|
||||
@GetMapping
|
||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
List<TagDto> dtos = tagService.searchTags(keyword).stream()
|
||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
||||
List<Tag> tags = tagService.searchTags(keyword);
|
||||
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
|
||||
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
|
||||
List<TagDto> dtos = tags.stream()
|
||||
.map(t -> tagMapper.toDto(t, postCntByTagIds.getOrDefault(t.getId(), 0L)))
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.collect(Collectors.toList());
|
||||
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||
|
||||
@@ -22,7 +22,7 @@ public class Notification {
|
||||
private Long id;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Column(nullable = false, length = 50)
|
||||
private NotificationType type;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
|
||||
@@ -92,8 +92,14 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
|
||||
long countByCategory_Id(Long categoryId);
|
||||
|
||||
@Query("SELECT c.id, COUNT(p) FROM Post p JOIN p.category c WHERE c.id IN :categoryIds GROUP BY c.id")
|
||||
List<Object[]> countPostsByCategoryIds(@Param("categoryIds") List<Long> categoryIds);
|
||||
|
||||
long countDistinctByTags_Id(Long tagId);
|
||||
|
||||
@Query("SELECT t.id, COUNT(DISTINCT p) FROM Post p JOIN p.tags t WHERE t.id IN :tagIds GROUP BY t.id")
|
||||
List<Object[]> countPostsByTagIds(@Param("tagIds") List<Long> tagIds);
|
||||
|
||||
long countByAuthor_Id(Long userId);
|
||||
|
||||
@Query("SELECT FUNCTION('date', p.createdAt) AS d, COUNT(p) AS c FROM Post p " +
|
||||
|
||||
@@ -31,16 +31,15 @@ import com.openisle.service.EmailSender;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
@@ -567,10 +566,31 @@ public class PostService {
|
||||
return postRepository.countByCategory_Id(categoryId);
|
||||
}
|
||||
|
||||
public Map<Long, Long> countPostsByCategoryIds(List<Long> categoryIds) {
|
||||
Map<Long, Long> result = new HashMap<>();
|
||||
var dbResult = postRepository.countPostsByCategoryIds(categoryIds);
|
||||
dbResult.forEach(r -> {
|
||||
result.put(((Long)r[0]), ((Long)r[1]));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public long countPostsByTag(Long tagId) {
|
||||
return postRepository.countDistinctByTags_Id(tagId);
|
||||
}
|
||||
|
||||
public Map<Long, Long> countPostsByTagIds(List<Long> tagIds) {
|
||||
Map<Long, Long> result = new HashMap<>();
|
||||
if (CollectionUtils.isEmpty(tagIds)) {
|
||||
return result;
|
||||
}
|
||||
var dbResult = postRepository.countPostsByTagIds(tagIds);
|
||||
dbResult.forEach(r -> {
|
||||
result.put(((Long)r[0]), ((Long)r[1]));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private java.util.List<Post> sortByPinnedAndCreated(java.util.List<Post> posts) {
|
||||
return posts.stream()
|
||||
.sorted(java.util.Comparator
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</button>
|
||||
<span v-if="isMobile && unreadCount > 0" class="menu-unread-dot"></span>
|
||||
</div>
|
||||
<NuxtLink class="logo-container" to="/" @click.prevent="goToHome">
|
||||
<NuxtLink class="logo-container" :to="`/`">
|
||||
<img
|
||||
alt="OpenIsle"
|
||||
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
||||
@@ -51,7 +51,6 @@
|
||||
<script setup>
|
||||
import { ClientOnly } from '#components'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||
import SearchDropdown from '~/components/SearchDropdown.vue'
|
||||
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
||||
@@ -67,20 +66,12 @@ const props = defineProps({
|
||||
const isLogin = computed(() => authState.loggedIn)
|
||||
const isMobile = useIsMobile()
|
||||
const unreadCount = computed(() => notificationState.unreadCount)
|
||||
const router = useRouter()
|
||||
const avatar = ref('')
|
||||
const showSearch = ref(false)
|
||||
const searchDropdown = ref(null)
|
||||
const userMenu = ref(null)
|
||||
const menuBtn = ref(null)
|
||||
|
||||
const goToHome = async () => {
|
||||
if (router.currentRoute.value.fullPath === '/') {
|
||||
window.dispatchEvent(new Event('refresh-home'))
|
||||
} else {
|
||||
await navigateTo('/', { replace: true })
|
||||
}
|
||||
}
|
||||
const search = () => {
|
||||
showSearch.value = true
|
||||
nextTick(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<transition name="slide">
|
||||
<nav v-if="visible" class="menu">
|
||||
<div class="menu-item-container">
|
||||
<NuxtLink class="menu-item" exact-active-class="selected" to="/" @click="handleHomeClick">
|
||||
<NuxtLink class="menu-item" exact-active-class="selected" to="/" @click="handleItemClick">
|
||||
<i class="menu-item-icon fas fa-hashtag"></i>
|
||||
<span class="menu-item-text">话题</span>
|
||||
</NuxtLink>
|
||||
@@ -191,10 +191,6 @@ onMounted(async () => {
|
||||
watch(() => authState.loggedIn, updateCount)
|
||||
})
|
||||
|
||||
const handleHomeClick = () => {
|
||||
navigateTo('/', { replace: true })
|
||||
}
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth <= 768) emit('item-click')
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ export default defineNuxtConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
baseURL: '/',
|
||||
buildAssetsDir: '/_nuxt/',
|
||||
},
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
|
||||
@@ -265,7 +265,7 @@ const {
|
||||
time: TimeManager.format(
|
||||
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
||||
),
|
||||
pinned: !!p.pinnedAt,
|
||||
pinned: Boolean(p.pinned ?? p.pinnedAt ?? p.pinned_at),
|
||||
type: p.type,
|
||||
}))
|
||||
},
|
||||
@@ -308,7 +308,7 @@ const fetchNextPage = async () => {
|
||||
time: TimeManager.format(
|
||||
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
||||
),
|
||||
pinned: !!p.pinnedAt,
|
||||
pinned: Boolean(p.pinned ?? p.pinnedAt ?? p.pinned_at),
|
||||
type: p.type,
|
||||
}))
|
||||
articles.value.push(...mapped)
|
||||
|
||||
@@ -392,7 +392,7 @@ const mapComment = (c, parentUserName = '', level = 0) => ({
|
||||
avatar: c.author.avatar,
|
||||
text: c.content,
|
||||
reactions: c.reactions || [],
|
||||
pinned: !!c.pinnedAt,
|
||||
pinned: Boolean(c.pinned ?? c.pinnedAt ?? c.pinned_at),
|
||||
reply: (c.replies || []).map((r) => mapComment(r, c.author.username, level + 1)),
|
||||
openReplies: level === 0,
|
||||
src: c.author.avatar,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
import ClickOutside from '~/directives/clickOutside.js'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
|
||||
9
frontend_nuxt/plugins/soft-manifest.client.ts
Normal file
9
frontend_nuxt/plugins/soft-manifest.client.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
// 覆盖默认行为:收到 manifest 更新时,不立刻在路由切换里刷新
|
||||
nuxtApp.hooks.hook('app:manifest:update', () => {
|
||||
// todo 选择:弹个提示,让用户点击刷新;或延迟到页面隐藏时再刷新
|
||||
// 例如:document.addEventListener('visibilitychange', () => { if (document.hidden) location.reload() })
|
||||
})
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
push(path) {
|
||||
if (process.client) {
|
||||
window.location.href = path
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user