mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-07-27 14:56:38 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84b45f785d | |||
| df56d7e885 | |||
| 76176e135c | |||
| ab87e0e51c | |||
| 5346a063bf | |||
| e53f2130b8 | |||
| 1e87e9252d | |||
| 3fc4d29dce | |||
| bcdac9d9b2 | |||
| ea9710d16f | |||
| 47134cadc2 | |||
| 1a1b20b9cf | |||
| b63ebb8fae | |||
| e0f7299a86 | |||
| 1f9ae8d057 | |||
| da1ad73cf6 | |||
| 53c603f33a | |||
| 06f86f2b21 | |||
| 0058f20b1e | |||
| 3dbcd2ac4d | |||
| 08239a16b8 | |||
| 43d4c9be43 |
@@ -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
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/* Maple Mono - Thin 100 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-100-normal.woff2") format("woff2");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Thin Italic 100 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-100-italic.woff2") format("woff2");
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - ExtraLight 200 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-200-normal.woff2") format("woff2");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - ExtraLight Italic 200 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-200-italic.woff2") format("woff2");
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Light 300 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-300-normal.woff2") format("woff2");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Light Italic 300 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-300-italic.woff2") format("woff2");
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Regular 400 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-400-normal.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Italic 400 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-400-italic.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Medium 500 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-500-normal.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Medium Italic 500 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-500-italic.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - SemiBold 600 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-600-normal.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - SemiBold Italic 600 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-600-italic.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Bold 700 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-700-normal.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - Bold Italic 700 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-700-italic.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - ExtraBold 800 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-800-normal.woff2") format("woff2");
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Maple Mono - ExtraBold Italic 800 */
|
||||
@font-face {
|
||||
font-family: "Maple Mono";
|
||||
src: url("/fonts/maple-mono-800-italic.woff2") format("woff2");
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -131,13 +131,43 @@ body {
|
||||
}
|
||||
|
||||
.info-content-text pre {
|
||||
background-color: var(--normal-background-color);
|
||||
display: flex;
|
||||
background-color: var(--lottery-background-color);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-content-text pre .line-numbers {
|
||||
counter-reset: line-number 0;
|
||||
width: 2em;
|
||||
font-size: 13px;
|
||||
position: sticky;
|
||||
flex-shrink: 0;
|
||||
font-family: 'Maple Mono', monospace;
|
||||
margin: 1em 0;
|
||||
color: #888;
|
||||
border-right: 1px solid #888;
|
||||
box-sizing: border-box;
|
||||
padding-right: 0.5em;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.info-content-text pre .line-numbers .line-number::before {
|
||||
content: counter(line-number);
|
||||
counter-increment: line-number;
|
||||
}
|
||||
|
||||
.info-content-text code {
|
||||
font-family: 'Maple Mono', monospace;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
white-space: no-wrap;
|
||||
background-color: var(--lottery-background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
@@ -156,15 +186,6 @@ body {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.info-content-text code {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--normal-background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.info-content-text a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
@@ -267,7 +288,7 @@ body {
|
||||
}
|
||||
|
||||
.info-content-text pre {
|
||||
line-height: 1.1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vditor-panel {
|
||||
|
||||
@@ -146,14 +146,6 @@ onMounted(async () => {
|
||||
await updateUnread()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.fullPath,
|
||||
() => {
|
||||
if (userMenu.value) userMenu.value.close()
|
||||
showSearch.value = false
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ export default defineNuxtConfig({
|
||||
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
||||
},
|
||||
},
|
||||
// Ensure Vditor styles load before our overrides in global.css
|
||||
css: ['vditor/dist/index.css', '~/assets/global.css'],
|
||||
// 确保 Vditor 样式在 global.css 覆盖前加载
|
||||
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
||||
app: {
|
||||
head: {
|
||||
script: [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import FlatPickr from 'vue-flatpickr-component'
|
||||
|
||||
@@ -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,9 +0,0 @@
|
||||
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() })
|
||||
})
|
||||
})
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,14 @@
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github.css'
|
||||
if (typeof window !== 'undefined') {
|
||||
const theme =
|
||||
document.documentElement.dataset.theme ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
if (theme === 'dark') {
|
||||
import('highlight.js/styles/atom-one-dark.css')
|
||||
} else {
|
||||
import('highlight.js/styles/atom-one-light.css')
|
||||
}
|
||||
}
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { toast } from '../main'
|
||||
import { tiebaEmoji } from './tiebaEmoji'
|
||||
@@ -86,7 +95,11 @@ const md = new MarkdownIt({
|
||||
} else {
|
||||
code = hljs.highlightAuto(str).value
|
||||
}
|
||||
return `<pre class="code-block"><button class="copy-code-btn">Copy</button><code class="hljs language-${lang || ''}">${code}</code></pre>`
|
||||
const lineNumbers = code
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(() => `<div class="line-number"></div>`)
|
||||
return `<pre class="code-block"><button class="copy-code-btn">Copy</button><div class="line-numbers">${lineNumbers.join('')}</div><code class="hljs language-${lang || ''}">${code.trim()}</code></pre>`
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user