mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-26 16:10:49 +08:00
Compare commits
43 Commits
codex/fix-
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f3830b3f7 | ||
|
|
2cf89e4802 | ||
|
|
1fc6460ae0 | ||
|
|
a04e5c2f6f | ||
|
|
77b26937f5 | ||
|
|
a1134b9d4b | ||
|
|
600f6ac1d1 | ||
|
|
9ad50b35c9 | ||
|
|
867ee3907b | ||
|
|
58fcd42745 | ||
|
|
0ee62a3a04 | ||
|
|
f0bc7a22a0 | ||
|
|
f6c0c8e226 | ||
|
|
8f3c0d6710 | ||
|
|
4f738778db | ||
|
|
84b45f785d | ||
|
|
df56d7e885 | ||
|
|
76176e135c | ||
|
|
ab87e0e51c | ||
|
|
5346a063bf | ||
|
|
e53f2130b8 | ||
|
|
1e87e9252d | ||
|
|
3fc4d29dce | ||
|
|
bcdac9d9b2 | ||
|
|
ea9710d16f | ||
|
|
47134cadc2 | ||
|
|
1a1b20b9cf | ||
|
|
b63ebb8fae | ||
|
|
e0f7299a86 | ||
|
|
1f9ae8d057 | ||
|
|
da1ad73cf6 | ||
|
|
53c603f33a | ||
|
|
06f86f2b21 | ||
|
|
22693bfdd9 | ||
|
|
0058f20b1e | ||
|
|
304d941d68 | ||
|
|
3dbcd2ac4d | ||
|
|
2efe4e733a | ||
|
|
08239a16b8 | ||
|
|
cb49dc9b73 | ||
|
|
43d4c9be43 | ||
|
|
1dc13698ad | ||
|
|
d58432dcd9 |
@@ -81,8 +81,8 @@ public class SecurityConfig {
|
|||||||
"http://localhost",
|
"http://localhost",
|
||||||
"http://30.211.97.238:3000",
|
"http://30.211.97.238:3000",
|
||||||
"http://30.211.97.238",
|
"http://30.211.97.238",
|
||||||
"http://192.168.7.70",
|
"http://192.168.7.98",
|
||||||
"http://192.168.7.70:8080",
|
"http://192.168.7.98:3000",
|
||||||
websiteUrl,
|
websiteUrl,
|
||||||
websiteUrl.replace("://www.", "://")
|
websiteUrl.replace("://www.", "://")
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -44,8 +45,11 @@ public class CategoryController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<CategoryDto> list() {
|
public List<CategoryDto> list() {
|
||||||
return categoryService.listCategories().stream()
|
List<Category> all = categoryService.listCategories();
|
||||||
.map(c -> categoryMapper.toDto(c, postService.countPostsByCategory(c.getId())))
|
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()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -62,8 +63,11 @@ public class TagController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
List<TagDto> dtos = tagService.searchTags(keyword).stream()
|
List<Tag> tags = tagService.searchTags(keyword);
|
||||||
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
|
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()))
|
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (limit != null && limit > 0 && dtos.size() > limit) {
|
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class Notification {
|
|||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false, length = 50)
|
||||||
private NotificationType type;
|
private NotificationType type;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
|||||||
@@ -92,8 +92,14 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
|||||||
|
|
||||||
long countByCategory_Id(Long categoryId);
|
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);
|
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);
|
long countByAuthor_Id(Long userId);
|
||||||
|
|
||||||
@Query("SELECT FUNCTION('date', p.createdAt) AS d, COUNT(p) AS c FROM Post p " +
|
@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.ZoneId;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
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.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
@@ -567,10 +566,31 @@ public class PostService {
|
|||||||
return postRepository.countByCategory_Id(categoryId);
|
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) {
|
public long countPostsByTag(Long tagId) {
|
||||||
return postRepository.countDistinctByTags_Id(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) {
|
private java.util.List<Post> sortByPinnedAndCreated(java.util.List<Post> posts) {
|
||||||
return posts.stream()
|
return posts.stream()
|
||||||
.sorted(java.util.Comparator
|
.sorted(java.util.Comparator
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||||
NUXT_PUBLIC_WEBSITE_BASE_URL=https://www.open-isle.com
|
NUXT_PUBLIC_WEBSITE_BASE_URL=https://www.open-isle.com
|
||||||
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-xxx.apps.googleusercontent.com
|
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
|
||||||
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
|
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
|
||||||
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
||||||
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
||||||
|
|||||||
143
frontend_nuxt/assets/fonts.css
Normal file
143
frontend_nuxt/assets/fonts.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
--scroller-background-color: rgba(130, 175, 180, 0.5);
|
--scroller-background-color: rgba(130, 175, 180, 0.5);
|
||||||
--normal-background-color: rgb(241, 241, 241);
|
--normal-background-color: rgb(241, 241, 241);
|
||||||
--lottery-background-color: rgb(241, 241, 241);
|
--lottery-background-color: rgb(241, 241, 241);
|
||||||
|
--code-highlight-background-color: rgb(241, 241, 241);
|
||||||
--login-background-color: rgb(248, 248, 248);
|
--login-background-color: rgb(248, 248, 248);
|
||||||
--login-background-color-hover: #e0e0e0;
|
--login-background-color-hover: #e0e0e0;
|
||||||
--text-color: black;
|
--text-color: black;
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
--menu-text-color: white;
|
--menu-text-color: white;
|
||||||
--normal-background-color: #000000;
|
--normal-background-color: #000000;
|
||||||
--lottery-background-color: #4e4e4e;
|
--lottery-background-color: #4e4e4e;
|
||||||
|
--code-highlight-background-color: #262b35;
|
||||||
--login-background-color: #575757;
|
--login-background-color: #575757;
|
||||||
--login-background-color-hover: #717171;
|
--login-background-color-hover: #717171;
|
||||||
--text-color: #eee;
|
--text-color: #eee;
|
||||||
@@ -131,13 +133,43 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-content-text pre {
|
.info-content-text pre {
|
||||||
background-color: var(--normal-background-color);
|
display: flex;
|
||||||
|
background-color: var(--code-highlight-background-color);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
position: relative;
|
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(--code-highlight-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
.copy-code-btn {
|
.copy-code-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
@@ -156,15 +188,6 @@ body {
|
|||||||
opacity: 1;
|
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 {
|
.info-content-text a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -267,7 +290,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-content-text pre {
|
.info-content-text pre {
|
||||||
line-height: 1.1;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vditor-panel {
|
.vditor-panel {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export default {
|
|||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.dropdown-item:hover {
|
.dropdown-item:hover {
|
||||||
background-color: var(--menu-selected-background-color);
|
background-color: var(--menu-selected-background-color);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<span v-if="isMobile && unreadCount > 0" class="menu-unread-dot"></span>
|
<span v-if="isMobile && unreadCount > 0" class="menu-unread-dot"></span>
|
||||||
</div>
|
</div>
|
||||||
<NuxtLink class="logo-container" to="/" replace @click="handleLogoClick">
|
<NuxtLink class="logo-container" :to="`/`">
|
||||||
<img
|
<img
|
||||||
alt="OpenIsle"
|
alt="OpenIsle"
|
||||||
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
||||||
@@ -51,7 +51,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ClientOnly } from '#components'
|
import { ClientOnly } from '#components'
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||||
import SearchDropdown from '~/components/SearchDropdown.vue'
|
import SearchDropdown from '~/components/SearchDropdown.vue'
|
||||||
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
||||||
@@ -67,19 +66,12 @@ const props = defineProps({
|
|||||||
const isLogin = computed(() => authState.loggedIn)
|
const isLogin = computed(() => authState.loggedIn)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const unreadCount = computed(() => notificationState.unreadCount)
|
const unreadCount = computed(() => notificationState.unreadCount)
|
||||||
const router = useRouter()
|
|
||||||
const avatar = ref('')
|
const avatar = ref('')
|
||||||
const showSearch = ref(false)
|
const showSearch = ref(false)
|
||||||
const searchDropdown = ref(null)
|
const searchDropdown = ref(null)
|
||||||
const userMenu = ref(null)
|
const userMenu = ref(null)
|
||||||
const menuBtn = ref(null)
|
const menuBtn = ref(null)
|
||||||
|
|
||||||
const handleLogoClick = (event) => {
|
|
||||||
if (router.currentRoute.value.fullPath === '/') {
|
|
||||||
event.preventDefault()
|
|
||||||
window.dispatchEvent(new Event('refresh-home'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
showSearch.value = true
|
showSearch.value = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -154,14 +146,6 @@ onMounted(async () => {
|
|||||||
await updateUnread()
|
await updateUnread()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => router.currentRoute.value.fullPath,
|
|
||||||
() => {
|
|
||||||
if (userMenu.value) userMenu.value.close()
|
|
||||||
showSearch.value = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<nav v-if="visible" class="menu">
|
<nav v-if="visible" class="menu">
|
||||||
<div class="menu-item-container">
|
<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>
|
<i class="menu-item-icon fas fa-hashtag"></i>
|
||||||
<span class="menu-item-text">话题</span>
|
<span class="menu-item-text">话题</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -128,41 +128,46 @@ import { ref, computed, watch, onMounted } from 'vue'
|
|||||||
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
|
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
|
||||||
import { authState } from '~/utils/auth'
|
import { authState } from '~/utils/auth'
|
||||||
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: { type: Boolean, default: true },
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['item-click'])
|
const emit = defineEmits(['item-click'])
|
||||||
|
|
||||||
const categoryOpen = ref(true)
|
const categoryOpen = ref(true)
|
||||||
const tagOpen = ref(true)
|
const tagOpen = ref(true)
|
||||||
const isLoadingCategory = ref(false)
|
|
||||||
const isLoadingTag = ref(false)
|
|
||||||
const categoryData = ref([])
|
|
||||||
const tagData = ref([])
|
|
||||||
|
|
||||||
const fetchCategoryData = async () => {
|
/** ✅ 用 useAsyncData 替换原生 fetch,避免 SSR+CSR 二次请求 */
|
||||||
isLoadingCategory.value = true
|
const {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/categories`)
|
data: categoryData,
|
||||||
const data = await res.json()
|
pending: isLoadingCategory,
|
||||||
categoryData.value = data
|
error: categoryError,
|
||||||
isLoadingCategory.value = false
|
} = await useAsyncData(
|
||||||
}
|
// 稳定 key:避免 hydration 期误判
|
||||||
|
'menu:categories',
|
||||||
|
() => $fetch(`${API_BASE_URL}/api/categories`),
|
||||||
|
{
|
||||||
|
server: true, // SSR 预取
|
||||||
|
default: () => [], // 初始默认值,减少空判断
|
||||||
|
// 5 分钟内复用缓存,避免路由往返重复请求
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const fetchTagData = async () => {
|
const {
|
||||||
isLoadingTag.value = true
|
data: tagData,
|
||||||
const res = await fetch(`${API_BASE_URL}/api/tags?limit=10`)
|
pending: isLoadingTag,
|
||||||
const data = await res.json()
|
error: tagError,
|
||||||
tagData.value = data
|
} = await useAsyncData('menu:tags', () => $fetch(`${API_BASE_URL}/api/tags?limit=10`), {
|
||||||
isLoadingTag.value = false
|
server: true,
|
||||||
}
|
default: () => [],
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 其余逻辑保持不变 */
|
||||||
const iconClass = computed(() => {
|
const iconClass = computed(() => {
|
||||||
switch (themeState.mode) {
|
switch (themeState.mode) {
|
||||||
case ThemeMode.DARK:
|
case ThemeMode.DARK:
|
||||||
@@ -188,13 +193,10 @@ const updateCount = async () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await updateCount()
|
await updateCount()
|
||||||
|
// 登录态变化时再拉一次未读数;与 useAsyncData 无关
|
||||||
watch(() => authState.loggedIn, updateCount)
|
watch(() => authState.loggedIn, updateCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleHomeClick = () => {
|
|
||||||
navigateTo('/', { replace: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleItemClick = () => {
|
const handleItemClick = () => {
|
||||||
if (window.innerWidth <= 768) emit('item-click')
|
if (window.innerWidth <= 768) emit('item-click')
|
||||||
}
|
}
|
||||||
@@ -215,8 +217,6 @@ const gotoTag = (t) => {
|
|||||||
navigateTo({ path: '/', query: { tags: value } }, { replace: true })
|
navigateTo({ path: '/', query: { tags: value } }, { replace: true })
|
||||||
handleItemClick()
|
handleItemClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([fetchCategoryData(), fetchTagData()])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ watch(selected, (val) => {
|
|||||||
selected.value = null
|
selected.value = null
|
||||||
keyword.value = ''
|
keyword.value = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggle,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export default defineNuxtConfig({
|
|||||||
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Ensure Vditor styles load before our overrides in global.css
|
// 确保 Vditor 样式在 global.css 覆盖前加载
|
||||||
css: ['vditor/dist/index.css', '~/assets/global.css'],
|
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
script: [
|
script: [
|
||||||
@@ -52,6 +52,8 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
baseURL: '/',
|
||||||
|
buildAssetsDir: '/_nuxt/',
|
||||||
},
|
},
|
||||||
vue: {
|
vue: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div class="forgot-page">
|
<div class="forgot-page">
|
||||||
<div class="forgot-content">
|
<div class="forgot-content">
|
||||||
<div class="forgot-title">找回密码</div>
|
<div class="forgot-title">找回密码</div>
|
||||||
|
|
||||||
<div v-if="step === 0" class="step-content">
|
<div v-if="step === 0" class="step-content">
|
||||||
<BaseInput icon="fas fa-envelope" v-model="email" placeholder="邮箱" />
|
<BaseInput icon="fas fa-envelope" v-model="email" placeholder="邮箱" />
|
||||||
<div v-if="emailError" class="error-message">{{ emailError }}</div>
|
<div v-if="emailError" class="error-message">{{ emailError }}</div>
|
||||||
@@ -19,6 +20,10 @@
|
|||||||
<div class="primary-button" @click="resetPassword" v-if="!isResetting">重置密码</div>
|
<div class="primary-button" @click="resetPassword" v-if="!isResetting">重置密码</div>
|
||||||
<div class="primary-button disabled" v-else>提交中...</div>
|
<div class="primary-button disabled" v-else>提交中...</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hint-message">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
使用 Google 注册的用户可使用对应的邮箱进行找回密码
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -26,6 +31,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
@@ -39,6 +46,7 @@ const passwordError = ref('')
|
|||||||
const isSending = ref(false)
|
const isSending = ref(false)
|
||||||
const isVerifying = ref(false)
|
const isVerifying = ref(false)
|
||||||
const isResetting = ref(false)
|
const isResetting = ref(false)
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (route.query.email) {
|
if (route.query.email) {
|
||||||
@@ -137,6 +145,21 @@ const resetPassword = async () => {
|
|||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forgot-content .hint-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--blockquote-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-message i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
.step-content {
|
.step-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, watchEffect, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||||
import ArticleTags from '~/components/ArticleTags.vue'
|
import ArticleTags from '~/components/ArticleTags.vue'
|
||||||
import CategorySelect from '~/components/CategorySelect.vue'
|
import CategorySelect from '~/components/CategorySelect.vue'
|
||||||
@@ -195,7 +195,7 @@ watch(
|
|||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/categories/${selectedCategory.value}`)
|
const res = await fetch(`${API_BASE_URL}/api/categories/`)
|
||||||
if (res.ok) categoryOptions.value = [await res.json()]
|
if (res.ok) categoryOptions.value = [await res.json()]
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,7 @@ const {
|
|||||||
time: TimeManager.format(
|
time: TimeManager.format(
|
||||||
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
||||||
),
|
),
|
||||||
pinned: !!p.pinnedAt,
|
pinned: Boolean(p.pinned ?? p.pinnedAt ?? p.pinned_at),
|
||||||
type: p.type,
|
type: p.type,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
@@ -308,7 +308,7 @@ const fetchNextPage = async () => {
|
|||||||
time: TimeManager.format(
|
time: TimeManager.format(
|
||||||
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt,
|
||||||
),
|
),
|
||||||
pinned: !!p.pinnedAt,
|
pinned: Boolean(p.pinned ?? p.pinnedAt ?? p.pinned_at),
|
||||||
type: p.type,
|
type: p.type,
|
||||||
}))
|
}))
|
||||||
articles.value.push(...mapped)
|
articles.value.push(...mapped)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import FlatPickr from 'vue-flatpickr-component'
|
import FlatPickr from 'vue-flatpickr-component'
|
||||||
|
|||||||
@@ -232,7 +232,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, watchEffect } from 'vue'
|
||||||
import VueEasyLightbox from 'vue-easy-lightbox'
|
import VueEasyLightbox from 'vue-easy-lightbox'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import CommentItem from '~/components/CommentItem.vue'
|
import CommentItem from '~/components/CommentItem.vue'
|
||||||
@@ -268,7 +268,6 @@ const postReactions = ref([])
|
|||||||
const comments = ref([])
|
const comments = ref([])
|
||||||
const status = ref('PUBLISHED')
|
const status = ref('PUBLISHED')
|
||||||
const pinnedAt = ref(null)
|
const pinnedAt = ref(null)
|
||||||
const isWaitingFetchingPost = ref(false)
|
|
||||||
const isWaitingPostingComment = ref(false)
|
const isWaitingPostingComment = ref(false)
|
||||||
const postTime = ref('')
|
const postTime = ref('')
|
||||||
const postItems = ref([])
|
const postItems = ref([])
|
||||||
@@ -392,7 +391,7 @@ const mapComment = (c, parentUserName = '', level = 0) => ({
|
|||||||
avatar: c.author.avatar,
|
avatar: c.author.avatar,
|
||||||
text: c.content,
|
text: c.content,
|
||||||
reactions: c.reactions || [],
|
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)),
|
reply: (c.replies || []).map((r) => mapComment(r, c.author.username, level + 1)),
|
||||||
openReplies: level === 0,
|
openReplies: level === 0,
|
||||||
src: c.author.avatar,
|
src: c.author.avatar,
|
||||||
@@ -455,38 +454,41 @@ const onCommentDeleted = (id) => {
|
|||||||
fetchComments()
|
fetchComments()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPost = async () => {
|
const {
|
||||||
try {
|
data: postData,
|
||||||
isWaitingFetchingPost.value = true
|
pending: pendingPost,
|
||||||
const token = getToken()
|
error: postError,
|
||||||
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}`, {
|
refresh: refreshPost,
|
||||||
headers: { Authorization: token ? `Bearer ${token}` : '' },
|
} = await useAsyncData(`post-${postId}`, () => $fetch(`${API_BASE_URL}/api/posts/${postId}`), {
|
||||||
})
|
server: true,
|
||||||
isWaitingFetchingPost.value = false
|
lazy: false,
|
||||||
if (!res.ok) {
|
})
|
||||||
if (res.status === 404 && process.client) {
|
|
||||||
router.replace('/404')
|
// 用 pendingPost 驱动现有 UI(替代 isWaitingFetchingPost 手控)
|
||||||
}
|
const isWaitingFetchingPost = computed(() => pendingPost.value)
|
||||||
return
|
|
||||||
}
|
// 同步到现有的响应式字段
|
||||||
const data = await res.json()
|
watchEffect(() => {
|
||||||
postContent.value = data.content
|
const data = postData.value
|
||||||
author.value = data.author
|
if (!data) return
|
||||||
title.value = data.title
|
postContent.value = data.content
|
||||||
category.value = data.category
|
author.value = data.author
|
||||||
tags.value = data.tags || []
|
title.value = data.title
|
||||||
postReactions.value = data.reactions || []
|
category.value = data.category
|
||||||
subscribed.value = !!data.subscribed
|
tags.value = data.tags || []
|
||||||
status.value = data.status
|
postReactions.value = data.reactions || []
|
||||||
pinnedAt.value = data.pinnedAt
|
subscribed.value = !!data.subscribed
|
||||||
postTime.value = TimeManager.format(data.createdAt)
|
status.value = data.status
|
||||||
lottery.value = data.lottery || null
|
pinnedAt.value = data.pinnedAt
|
||||||
if (lottery.value && lottery.value.endTime) startCountdown()
|
postTime.value = TimeManager.format(data.createdAt)
|
||||||
await nextTick()
|
lottery.value = data.lottery || null
|
||||||
} catch (e) {
|
if (lottery.value && lottery.value.endTime) startCountdown()
|
||||||
console.error(e)
|
})
|
||||||
}
|
|
||||||
}
|
// 404 客户端跳转
|
||||||
|
// if (postError.value?.statusCode === 404 && process.client) {
|
||||||
|
// router.replace('/404')
|
||||||
|
// }
|
||||||
|
|
||||||
const totalPosts = computed(() => comments.value.length + 1)
|
const totalPosts = computed(() => comments.value.length + 1)
|
||||||
const lastReplyTime = computed(() =>
|
const lastReplyTime = computed(() =>
|
||||||
@@ -607,6 +609,7 @@ const approvePost = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
status.value = 'PUBLISHED'
|
status.value = 'PUBLISHED'
|
||||||
toast.success('已通过审核')
|
toast.success('已通过审核')
|
||||||
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -620,8 +623,8 @@ const pinPost = async () => {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
pinnedAt.value = new Date().toISOString()
|
|
||||||
toast.success('已置顶')
|
toast.success('已置顶')
|
||||||
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -635,8 +638,8 @@ const unpinPost = async () => {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
pinnedAt.value = null
|
|
||||||
toast.success('已取消置顶')
|
toast.success('已取消置顶')
|
||||||
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -674,6 +677,7 @@ const rejectPost = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
status.value = 'REJECTED'
|
status.value = 'REJECTED'
|
||||||
toast.success('已驳回')
|
toast.success('已驳回')
|
||||||
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -709,7 +713,7 @@ const joinLottery = async () => {
|
|||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('已参与抽奖')
|
toast.success('已参与抽奖')
|
||||||
await fetchPost()
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -780,9 +784,8 @@ onMounted(async () => {
|
|||||||
window.addEventListener('scroll', updateCurrentIndex)
|
window.addEventListener('scroll', updateCurrentIndex)
|
||||||
jumpToHashComment()
|
jumpToHashComment()
|
||||||
})
|
})
|
||||||
|
|
||||||
await fetchPost()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.post-page-container {
|
.post-page-container {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { defineNuxtPlugin } from 'nuxt/app'
|
||||||
import ClickOutside from '~/directives/clickOutside.js'
|
import ClickOutside from '~/directives/clickOutside.js'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
|||||||
BIN
frontend_nuxt/public/fonts/maple-mono-100-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-100-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-100-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-100-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-200-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-200-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-200-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-200-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-300-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-300-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-300-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-300-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-400-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-400-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-400-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-400-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-500-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-500-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-500-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-500-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-600-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-600-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-600-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-600-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-700-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-700-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-700-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-700-normal.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-800-italic.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-800-italic.woff2
Normal file
Binary file not shown.
BIN
frontend_nuxt/public/fonts/maple-mono-800-normal.woff2
Normal file
BIN
frontend_nuxt/public/fonts/maple-mono-800-normal.woff2
Normal file
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
export default {
|
|
||||||
push(path) {
|
|
||||||
if (process.client) {
|
|
||||||
window.location.href = path
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -24,6 +24,7 @@ export async function googleGetIdToken() {
|
|||||||
export function googleAuthorize() {
|
export function googleAuthorize() {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const GOOGLE_CLIENT_ID = config.public.googleClientId
|
const GOOGLE_CLIENT_ID = config.public.googleClientId
|
||||||
|
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||||
if (!GOOGLE_CLIENT_ID) {
|
if (!GOOGLE_CLIENT_ID) {
|
||||||
toast.error('Google 登录不可用, 请检查网络设置与VPN')
|
toast.error('Google 登录不可用, 请检查网络设置与VPN')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import hljs from 'highlight.js'
|
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 MarkdownIt from 'markdown-it'
|
||||||
import { toast } from '../main'
|
import { toast } from '../main'
|
||||||
import { tiebaEmoji } from './tiebaEmoji'
|
import { tiebaEmoji } from './tiebaEmoji'
|
||||||
@@ -86,7 +95,11 @@ const md = new MarkdownIt({
|
|||||||
} else {
|
} else {
|
||||||
code = hljs.highlightAuto(str).value
|
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