Compare commits

...

15 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
10 changed files with 109 additions and 71 deletions

View File

@@ -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.", "://")
)); ));

View File

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

View File

@@ -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;
@@ -132,7 +134,7 @@ body {
.info-content-text pre { .info-content-text pre {
display: flex; display: flex;
background-color: var(--lottery-background-color); 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;
@@ -164,7 +166,7 @@ body {
font-size: 13px; font-size: 13px;
border-radius: 4px; border-radius: 4px;
white-space: no-wrap; white-space: no-wrap;
background-color: var(--lottery-background-color); background-color: var(--code-highlight-background-color);
color: var(--text-color); color: var(--text-color);
} }

View File

@@ -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);

View File

@@ -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,6 +193,7 @@ const updateCount = async () => {
onMounted(async () => { onMounted(async () => {
await updateCount() await updateCount()
// 登录态变化时再拉一次未读数;与 useAsyncData 无关
watch(() => authState.loggedIn, updateCount) watch(() => authState.loggedIn, updateCount)
}) })
@@ -211,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>

View File

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

View File

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

View File

@@ -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 {}
} }

View File

@@ -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([])
@@ -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);

View File

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