Compare commits

..

28 Commits

Author SHA1 Message Date
tim
6f3830b3f7 fix: revert vditor change 2025-08-15 00:50:44 +08:00
tim
2cf89e4802 fix: ssr 水合采用useAsyncData 2025-08-15 00:12:06 +08:00
tim
1fc6460ae0 fix: 修复vditor移动端贴顶的问题 2025-08-15 00:01:18 +08:00
Tim
a04e5c2f6f Merge pull request #560 from CH-122/feat/password-recovery-hint
feat: 忘记密码页面添加提示 & 修复缺少定义导致的报错 #535
2025-08-14 23:43:26 +08:00
Tim
77b26937f5 Merge pull request #562 from CH-122/fix/mobile-header-search
fix: 移动端 header 点击搜索图标功能异常
2025-08-14 23:39:19 +08:00
Tim
a1134b9d4b Merge pull request #559 from AnNingUI/main 2025-08-14 21:42:32 +08:00
AnNingUI
600f6ac1d1 fix: 修复代码高亮背景与抽奖背景色公用的问题 2025-08-14 21:39:39 +08:00
CH_122
9ad50b35c9 fix: 移动端 header 点击搜索图标功能异常 2025-08-14 21:35:57 +08:00
CH_122
867ee3907b feat: 忘记密码添加提示 & 修复缺少定义导致的报错 2025-08-14 21:21:34 +08:00
CH_122
58fcd42745 style: add cursor pointer to dropdown items for better UX 2025-08-14 21:20:23 +08:00
AnNingUI
0ee62a3a04 fix: 让代码展示背景的样式更加现代化,修复分类选择框仅有一个当前分类的问题
Fixes #558
2025-08-14 21:05:08 +08:00
Tim
f0bc7a22a0 fix: google login 问题修复 2025-08-14 20:34:21 +08:00
Tim
f6c0c8e226 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-14 20:25:33 +08:00
Tim
8f3c0d6710 fix: google login 问题修复 2025-08-14 20:25:09 +08:00
Tim
4f738778db Merge pull request #557 from nagisa77/feature/code_buauty
fix: 代码风格设置
2025-08-14 20:17:23 +08:00
Tim
84b45f785d fix: 代码风格设置 2025-08-14 19:55:53 +08:00
tim
df56d7e885 Revert "optimize(backend): optimize /api/posts/latest-reply"
This reverts commit 1e87e9252d.
2025-08-14 18:54:12 +08:00
tim
76176e135c Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-14 18:27:25 +08:00
tim
ab87e0e51c fix: fix missing setup 2025-08-14 18:27:12 +08:00
Tim
5346a063bf Merge pull request #555 from netcaty/main
优化主页列表接口/api/posts/latest-reply
2025-08-14 18:19:19 +08:00
netcaty
e53f2130b8 Merge branch 'nagisa77:main' into main 2025-08-14 17:54:08 +08:00
netcat
1e87e9252d optimize(backend): optimize /api/posts/latest-reply
resolves #554
2025-08-14 17:53:01 +08:00
tim
3fc4d29dce Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-14 17:27:42 +08:00
tim
bcdac9d9b2 fix: delete hook update 2025-08-14 17:27:30 +08:00
Tim
ea9710d16f Merge pull request #553 from nagisa77/codex/fix-missing-comment-pinning-feature
fix: restore comment pin handling
2025-08-14 17:21:26 +08:00
tim
1a1b20b9cf fix: update css import 2025-08-14 17:20:02 +08:00
Tim
b63ebb8fae Merge pull request #552 from immortal521/feat/code-block-line-number
feat: add code block line number display
2025-08-14 16:47:46 +08:00
immortal521
e0f7299a86 feat: add code block line number display
- Added Maple Mono font
- Changed code block font to Maple Mono
- Increased mobile line height from 1.1 to 1.5
2025-08-14 15:40:14 +08:00
32 changed files with 300 additions and 102 deletions

View File

@@ -81,8 +81,8 @@ public class SecurityConfig {
"http://localhost",
"http://30.211.97.238:3000",
"http://30.211.97.238",
"http://192.168.7.70",
"http://192.168.7.70:8080",
"http://192.168.7.98",
"http://192.168.7.98:3000",
websiteUrl,
websiteUrl.replace("://www.", "://")
));

View File

@@ -1,6 +1,6 @@
NUXT_PUBLIC_API_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_DISCORD_CLIENT_ID=1394985417044000779
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ

View 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;
}

View File

@@ -17,6 +17,7 @@
--scroller-background-color: rgba(130, 175, 180, 0.5);
--normal-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-hover: #e0e0e0;
--text-color: black;
@@ -44,6 +45,7 @@
--menu-text-color: white;
--normal-background-color: #000000;
--lottery-background-color: #4e4e4e;
--code-highlight-background-color: #262b35;
--login-background-color: #575757;
--login-background-color-hover: #717171;
--text-color: #eee;
@@ -131,13 +133,43 @@ body {
}
.info-content-text pre {
background-color: var(--normal-background-color);
display: flex;
background-color: var(--code-highlight-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(--code-highlight-background-color);
color: var(--text-color);
}
.copy-code-btn {
position: absolute;
top: 4px;
@@ -156,15 +188,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 +290,7 @@ body {
}
.info-content-text pre {
line-height: 1.1;
line-height: 1.5;
}
.vditor-panel {

View File

@@ -82,6 +82,7 @@ export default {
.dropdown-item {
padding: 8px 16px;
white-space: nowrap;
cursor: pointer;
}
.dropdown-item:hover {
background-color: var(--menu-selected-background-color);

View File

@@ -146,14 +146,6 @@ onMounted(async () => {
await updateUnread()
},
)
watch(
() => router.currentRoute.value.fullPath,
() => {
if (userMenu.value) userMenu.value.close()
showSearch.value = false
},
)
})
</script>

View File

@@ -128,41 +128,46 @@ import { ref, computed, watch, onMounted } from 'vue'
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
import { authState } from '~/utils/auth'
import { fetchUnreadCount, notificationState } from '~/utils/notification'
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const props = defineProps({
visible: {
type: Boolean,
default: true,
},
visible: { type: Boolean, default: true },
})
const emit = defineEmits(['item-click'])
const categoryOpen = ref(true)
const tagOpen = ref(true)
const isLoadingCategory = ref(false)
const isLoadingTag = ref(false)
const categoryData = ref([])
const tagData = ref([])
const fetchCategoryData = async () => {
isLoadingCategory.value = true
const res = await fetch(`${API_BASE_URL}/api/categories`)
const data = await res.json()
categoryData.value = data
isLoadingCategory.value = false
}
/** ✅ 用 useAsyncData 替换原生 fetch避免 SSR+CSR 二次请求 */
const {
data: categoryData,
pending: isLoadingCategory,
error: categoryError,
} = await useAsyncData(
// 稳定 key避免 hydration 期误判
'menu:categories',
() => $fetch(`${API_BASE_URL}/api/categories`),
{
server: true, // SSR 预取
default: () => [], // 初始默认值,减少空判断
// 5 分钟内复用缓存,避免路由往返重复请求
staleTime: 5 * 60 * 1000,
},
)
const fetchTagData = async () => {
isLoadingTag.value = true
const res = await fetch(`${API_BASE_URL}/api/tags?limit=10`)
const data = await res.json()
tagData.value = data
isLoadingTag.value = false
}
const {
data: tagData,
pending: isLoadingTag,
error: tagError,
} = await useAsyncData('menu:tags', () => $fetch(`${API_BASE_URL}/api/tags?limit=10`), {
server: true,
default: () => [],
staleTime: 5 * 60 * 1000,
})
/** 其余逻辑保持不变 */
const iconClass = computed(() => {
switch (themeState.mode) {
case ThemeMode.DARK:
@@ -188,6 +193,7 @@ const updateCount = async () => {
onMounted(async () => {
await updateCount()
// 登录态变化时再拉一次未读数;与 useAsyncData 无关
watch(() => authState.loggedIn, updateCount)
})
@@ -211,8 +217,6 @@ const gotoTag = (t) => {
navigateTo({ path: '/', query: { tags: value } }, { replace: true })
handleItemClick()
}
await Promise.all([fetchCategoryData(), fetchTagData()])
</script>
<style scoped>

View File

@@ -110,6 +110,10 @@ watch(selected, (val) => {
selected.value = null
keyword.value = ''
})
defineExpose({
toggle,
})
</script>
<style scoped>

View File

@@ -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: [

View File

@@ -2,6 +2,7 @@
<div class="forgot-page">
<div class="forgot-content">
<div class="forgot-title">找回密码</div>
<div v-if="step === 0" class="step-content">
<BaseInput icon="fas fa-envelope" v-model="email" placeholder="邮箱" />
<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 disabled" v-else>提交中...</div>
</div>
<div class="hint-message">
<i class="fas fa-info-circle"></i>
使用 Google 注册的用户可使用对应的邮箱进行找回密码
</div>
</div>
</div>
</template>
@@ -26,6 +31,8 @@
<script setup>
import { toast } from '~/main'
import BaseInput from '~/components/BaseInput.vue'
import { useRoute } from 'vue-router'
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
@@ -39,6 +46,7 @@ const passwordError = ref('')
const isSending = ref(false)
const isVerifying = ref(false)
const isResetting = ref(false)
const route = useRoute()
onMounted(() => {
if (route.query.email) {
@@ -137,6 +145,21 @@ const resetPassword = async () => {
font-size: 24px;
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 {
display: flex;
flex-direction: column;

View File

@@ -116,7 +116,7 @@
</div>
</template>
<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 ArticleTags from '~/components/ArticleTags.vue'
import CategorySelect from '~/components/CategorySelect.vue'
@@ -195,7 +195,7 @@ watch(
const loadOptions = async () => {
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
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()]
} catch {}
}

View File

@@ -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'

View File

@@ -232,7 +232,7 @@
</template>
<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 { useRoute } from 'vue-router'
import CommentItem from '~/components/CommentItem.vue'
@@ -268,7 +268,6 @@ const postReactions = ref([])
const comments = ref([])
const status = ref('PUBLISHED')
const pinnedAt = ref(null)
const isWaitingFetchingPost = ref(false)
const isWaitingPostingComment = ref(false)
const postTime = ref('')
const postItems = ref([])
@@ -455,38 +454,41 @@ const onCommentDeleted = (id) => {
fetchComments()
}
const fetchPost = async () => {
try {
isWaitingFetchingPost.value = true
const token = getToken()
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}`, {
headers: { Authorization: token ? `Bearer ${token}` : '' },
})
isWaitingFetchingPost.value = false
if (!res.ok) {
if (res.status === 404 && process.client) {
router.replace('/404')
}
return
}
const data = await res.json()
postContent.value = data.content
author.value = data.author
title.value = data.title
category.value = data.category
tags.value = data.tags || []
postReactions.value = data.reactions || []
subscribed.value = !!data.subscribed
status.value = data.status
pinnedAt.value = data.pinnedAt
postTime.value = TimeManager.format(data.createdAt)
lottery.value = data.lottery || null
if (lottery.value && lottery.value.endTime) startCountdown()
await nextTick()
} catch (e) {
console.error(e)
}
}
const {
data: postData,
pending: pendingPost,
error: postError,
refresh: refreshPost,
} = await useAsyncData(`post-${postId}`, () => $fetch(`${API_BASE_URL}/api/posts/${postId}`), {
server: true,
lazy: false,
})
// 用 pendingPost 驱动现有 UI替代 isWaitingFetchingPost 手控)
const isWaitingFetchingPost = computed(() => pendingPost.value)
// 同步到现有的响应式字段
watchEffect(() => {
const data = postData.value
if (!data) return
postContent.value = data.content
author.value = data.author
title.value = data.title
category.value = data.category
tags.value = data.tags || []
postReactions.value = data.reactions || []
subscribed.value = !!data.subscribed
status.value = data.status
pinnedAt.value = data.pinnedAt
postTime.value = TimeManager.format(data.createdAt)
lottery.value = data.lottery || null
if (lottery.value && lottery.value.endTime) startCountdown()
})
// 404 客户端跳转
// if (postError.value?.statusCode === 404 && process.client) {
// router.replace('/404')
// }
const totalPosts = computed(() => comments.value.length + 1)
const lastReplyTime = computed(() =>
@@ -607,6 +609,7 @@ const approvePost = async () => {
if (res.ok) {
status.value = 'PUBLISHED'
toast.success('已通过审核')
await refreshPost()
} else {
toast.error('操作失败')
}
@@ -620,8 +623,8 @@ const pinPost = async () => {
headers: { Authorization: `Bearer ${token}` },
})
if (res.ok) {
pinnedAt.value = new Date().toISOString()
toast.success('已置顶')
await refreshPost()
} else {
toast.error('操作失败')
}
@@ -635,8 +638,8 @@ const unpinPost = async () => {
headers: { Authorization: `Bearer ${token}` },
})
if (res.ok) {
pinnedAt.value = null
toast.success('已取消置顶')
await refreshPost()
} else {
toast.error('操作失败')
}
@@ -674,6 +677,7 @@ const rejectPost = async () => {
if (res.ok) {
status.value = 'REJECTED'
toast.success('已驳回')
await refreshPost()
} else {
toast.error('操作失败')
}
@@ -709,7 +713,7 @@ const joinLottery = async () => {
})
if (res.ok) {
toast.success('已参与抽奖')
await fetchPost()
await refreshPost()
} else {
toast.error('操作失败')
}
@@ -780,9 +784,8 @@ onMounted(async () => {
window.addEventListener('scroll', updateCurrentIndex)
jumpToHashComment()
})
await fetchPost()
</script>
<style>
.post-page-container {
background-color: var(--background-color);

View File

@@ -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() })
})
})

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -24,6 +24,7 @@ export async function googleGetIdToken() {
export function googleAuthorize() {
const config = useRuntimeConfig()
const GOOGLE_CLIENT_ID = config.public.googleClientId
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
if (!GOOGLE_CLIENT_ID) {
toast.error('Google 登录不可用, 请检查网络设置与VPN')
return

View File

@@ -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>`
},
})