mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-05-08 11:47:28 +08:00
Merge pull request #201 from nagisa77/codex/add-404-page-and-disable-editors
Add 404 and login overlays
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import Vditor from 'vditor'
|
import Vditor from 'vditor'
|
||||||
import 'vditor/dist/index.css'
|
import 'vditor/dist/index.css'
|
||||||
|
|
||||||
@@ -34,13 +34,17 @@ export default {
|
|||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const vditorInstance = ref(null)
|
const vditorInstance = ref(null)
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
|
|
||||||
const isDisabled = computed(() => props.loading || !text.value.trim())
|
const isDisabled = computed(() => props.loading || props.disabled || !text.value.trim())
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!vditorInstance.value || isDisabled.value) return
|
if (!vditorInstance.value || isDisabled.value) return
|
||||||
@@ -79,12 +83,39 @@ export default {
|
|||||||
'image'
|
'image'
|
||||||
],
|
],
|
||||||
toolbarConfig: { pin: true },
|
toolbarConfig: { pin: true },
|
||||||
input(value) {
|
input(value) {
|
||||||
text.value = value
|
text.value = value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (props.disabled || props.loading) {
|
||||||
|
vditorInstance.value.disabled()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.loading,
|
||||||
|
val => {
|
||||||
|
if (!vditorInstance.value) return
|
||||||
|
if (val) {
|
||||||
|
vditorInstance.value.disabled()
|
||||||
|
} else if (!props.disabled) {
|
||||||
|
vditorInstance.value.enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.disabled,
|
||||||
|
val => {
|
||||||
|
if (!vditorInstance.value) return
|
||||||
|
if (val) {
|
||||||
|
vditorInstance.value.disabled()
|
||||||
|
} else if (!props.loading) {
|
||||||
|
vditorInstance.value.enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return { submit, isDisabled }
|
return { submit, isDisabled }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
open-isle-cli/src/components/LoginOverlay.vue
Normal file
39
open-isle-cli/src/components/LoginOverlay.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-overlay" @click="goLogin">
|
||||||
|
<div class="login-overlay-text">
|
||||||
|
请先登录,点击跳转到登录页面
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoginOverlay',
|
||||||
|
setup() {
|
||||||
|
const router = useRouter()
|
||||||
|
const goLogin = () => {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
return { goLogin }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,6 +32,10 @@ export default {
|
|||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
@@ -48,6 +52,18 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.disabled,
|
||||||
|
val => {
|
||||||
|
if (!vditorInstance.value) return
|
||||||
|
if (val) {
|
||||||
|
vditorInstance.value.disabled()
|
||||||
|
} else if (!props.loading) {
|
||||||
|
vditorInstance.value.enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
val => {
|
val => {
|
||||||
@@ -118,6 +134,9 @@ export default {
|
|||||||
},
|
},
|
||||||
after() {
|
after() {
|
||||||
vditorInstance.value.setValue(props.modelValue)
|
vditorInstance.value.setValue(props.modelValue)
|
||||||
|
if (props.loading || props.disabled) {
|
||||||
|
vditorInstance.value.disabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SignupPageView from '../views/SignupPageView.vue'
|
|||||||
import NewPostPageView from '../views/NewPostPageView.vue'
|
import NewPostPageView from '../views/NewPostPageView.vue'
|
||||||
import SettingsPageView from '../views/SettingsPageView.vue'
|
import SettingsPageView from '../views/SettingsPageView.vue'
|
||||||
import ProfileView from '../views/ProfileView.vue'
|
import ProfileView from '../views/ProfileView.vue'
|
||||||
|
import NotFoundPageView from '../views/NotFoundPageView.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -55,6 +56,15 @@ const routes = [
|
|||||||
name: 'users',
|
name: 'users',
|
||||||
component: ProfileView
|
component: ProfileView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
name: 'not-found',
|
||||||
|
component: NotFoundPageView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
redirect: '/404'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<div class="new-post-form">
|
<div class="new-post-form">
|
||||||
<input class="post-title-input" v-model="title" placeholder="标题" />
|
<input class="post-title-input" v-model="title" placeholder="标题" />
|
||||||
<div class="post-editor-container">
|
<div class="post-editor-container">
|
||||||
<PostEditor v-model="content" :loading="isAiLoading" />
|
<PostEditor v-model="content" :loading="isAiLoading" :disabled="!isLogin" />
|
||||||
|
<LoginOverlay v-if="!isLogin" />
|
||||||
</div>
|
</div>
|
||||||
<div class="post-options">
|
<div class="post-options">
|
||||||
<div class="post-options-left">
|
<div class="post-options-left">
|
||||||
@@ -22,7 +23,12 @@
|
|||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk"></i>
|
||||||
存草稿
|
存草稿
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isWaitingPosting" class="post-submit" @click="submitPost">发布</div>
|
<div
|
||||||
|
v-if="!isWaitingPosting"
|
||||||
|
class="post-submit"
|
||||||
|
:class="{ disabled: !isLogin }"
|
||||||
|
@click="isLogin && submitPost"
|
||||||
|
>发布</div>
|
||||||
<div v-else class="post-submit-loading"> <i class="fa-solid fa-spinner fa-spin"></i> 发布中...</div>
|
<div v-else class="post-submit-loading"> <i class="fa-solid fa-spinner fa-spin"></i> 发布中...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,16 +37,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import PostEditor from '../components/PostEditor.vue'
|
import PostEditor from '../components/PostEditor.vue'
|
||||||
import CategorySelect from '../components/CategorySelect.vue'
|
import CategorySelect from '../components/CategorySelect.vue'
|
||||||
import TagSelect from '../components/TagSelect.vue'
|
import TagSelect from '../components/TagSelect.vue'
|
||||||
import { API_BASE_URL, toast } from '../main'
|
import { API_BASE_URL, toast } from '../main'
|
||||||
import { getToken } from '../utils/auth'
|
import { getToken, authState } from '../utils/auth'
|
||||||
|
import LoginOverlay from '../components/LoginOverlay.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NewPostPageView',
|
name: 'NewPostPageView',
|
||||||
components: { PostEditor, CategorySelect, TagSelect },
|
components: { PostEditor, CategorySelect, TagSelect, LoginOverlay },
|
||||||
setup() {
|
setup() {
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const content = ref('')
|
const content = ref('')
|
||||||
@@ -48,6 +55,7 @@ export default {
|
|||||||
const selectedTags = ref([])
|
const selectedTags = ref([])
|
||||||
const isWaitingPosting = ref(false)
|
const isWaitingPosting = ref(false)
|
||||||
const isAiLoading = ref(false)
|
const isAiLoading = ref(false)
|
||||||
|
const isLogin = computed(() => authState.loggedIn)
|
||||||
|
|
||||||
const loadDraft = async () => {
|
const loadDraft = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
@@ -237,7 +245,7 @@ export default {
|
|||||||
isWaitingPosting.value = false
|
isWaitingPosting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { title, content, selectedCategory, selectedTags, submitPost, saveDraft, clearPost, isWaitingPosting, aiGenerate, isAiLoading }
|
return { title, content, selectedCategory, selectedTags, submitPost, saveDraft, clearPost, isWaitingPosting, aiGenerate, isAiLoading, isLogin }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -296,6 +304,10 @@ export default {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-editor-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.post-submit {
|
.post-submit {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -305,9 +317,17 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-submit.disabled {
|
||||||
|
background-color: var(--primary-color-disabled);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.post-submit:hover {
|
.post-submit:hover {
|
||||||
background-color: var(--primary-color-hover);
|
background-color: var(--primary-color-hover);
|
||||||
}
|
}
|
||||||
|
.post-submit.disabled:hover {
|
||||||
|
background-color: var(--primary-color-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
.post-submit-loading {
|
.post-submit-loading {
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
33
open-isle-cli/src/views/NotFoundPageView.vue
Normal file
33
open-isle-cli/src/views/NotFoundPageView.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-found-page">
|
||||||
|
<h1>404 - 页面不存在</h1>
|
||||||
|
<p>你访问的页面不存在或已被删除。</p>
|
||||||
|
<router-link to="/">返回首页</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NotFoundPageView'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-page h1 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-page a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -59,7 +59,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommentEditor @submit="postComment" :loading="isWaitingPostingComment" />
|
<div class="comment-editor-wrapper">
|
||||||
|
<CommentEditor
|
||||||
|
@submit="postComment"
|
||||||
|
:loading="isWaitingPostingComment"
|
||||||
|
:disabled="!loggedIn"
|
||||||
|
/>
|
||||||
|
<LoginOverlay v-if="!loggedIn" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="comments-container">
|
<div class="comments-container">
|
||||||
<BaseTimeline :items="comments">
|
<BaseTimeline :items="comments">
|
||||||
@@ -104,6 +111,7 @@ import ArticleTags from '../components/ArticleTags.vue'
|
|||||||
import ArticleCategory from '../components/ArticleCategory.vue'
|
import ArticleCategory from '../components/ArticleCategory.vue'
|
||||||
import ReactionsGroup from '../components/ReactionsGroup.vue'
|
import ReactionsGroup from '../components/ReactionsGroup.vue'
|
||||||
import DropdownMenu from '../components/DropdownMenu.vue'
|
import DropdownMenu from '../components/DropdownMenu.vue'
|
||||||
|
import LoginOverlay from '../components/LoginOverlay.vue'
|
||||||
import { renderMarkdown } from '../utils/markdown'
|
import { renderMarkdown } from '../utils/markdown'
|
||||||
import { API_BASE_URL, toast } from '../main'
|
import { API_BASE_URL, toast } from '../main'
|
||||||
import { getToken, authState } from '../utils/auth'
|
import { getToken, authState } from '../utils/auth'
|
||||||
@@ -114,7 +122,7 @@ hatch.register()
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PostPageView',
|
name: 'PostPageView',
|
||||||
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ArticleCategory, ReactionsGroup, DropdownMenu },
|
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ArticleCategory, ReactionsGroup, DropdownMenu, LoginOverlay },
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const postId = route.params.id
|
const postId = route.params.id
|
||||||
@@ -235,7 +243,12 @@ export default {
|
|||||||
headers: { Authorization: token ? `Bearer ${token}` : '' }
|
headers: { Authorization: token ? `Bearer ${token}` : '' }
|
||||||
})
|
})
|
||||||
isWaitingFetchingPost.value = false;
|
isWaitingFetchingPost.value = false;
|
||||||
if (!res.ok) return
|
if (!res.ok) {
|
||||||
|
if (res.status === 404) {
|
||||||
|
router.replace('/404')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
postContent.value = data.content
|
postContent.value = data.content
|
||||||
author.value = data.author
|
author.value = data.author
|
||||||
@@ -779,4 +792,8 @@ export default {
|
|||||||
.copy-link:hover {
|
.copy-link:hover {
|
||||||
background-color: #e2e2e2;
|
background-color: #e2e2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ export default {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
user.value = data
|
user.value = data
|
||||||
subscribed.value = !!data.subscribed
|
subscribed.value = !!data.subscribed
|
||||||
|
} else if (res.status === 404) {
|
||||||
|
router.replace('/404')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user