feat: add authentication protected encyclopedia routing, update relevant documents and configurations

This commit is contained in:
2026-07-18 16:20:51 +08:00
parent 613452b7fe
commit 8da565b36b
7 changed files with 35 additions and 17 deletions
+4 -3
View File
@@ -37,8 +37,8 @@
| `/` | MapView | No | | `/` | MapView | No |
| `/login`, `/register`, `/forgot-password`, `/auth/confirm`, `/auth/reset-password` | Auth views | No | | `/login`, `/register`, `/forgot-password`, `/auth/confirm`, `/auth/reset-password` | Auth views | No |
| `/upload` | UploadView | Yes | | `/upload` | UploadView | Yes |
| `/encyclopedia` | EncyclopediaView | No | | `/encyclopedia` | EncyclopediaView | Yes |
| `/encyclopedia/:id` | CloudTypeView | No | | `/encyclopedia/:id` | CloudTypeView | Yes |
| `/gallery` | GalleryView | No | | `/gallery` | GalleryView | No |
| `/community` | CommunityView | No | | `/community` | CommunityView | No |
| `/profile` | ProfileView (own) | Yes | | `/profile` | ProfileView (own) | Yes |
@@ -109,7 +109,8 @@
## Build & Deploy ## Build & Deploy
- **Vercel** with `vercel.json`: SPA rewrites, security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). No CSP or HSTS configured. - **Vercel** with `vercel.json`: SPA rewrites, security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). No CSP or HSTS configured.
- **SEO plugin** in `vite.config.ts`: generates `robots.txt` and `sitemap.xml` at build time. Disallows `/admin`, `/auth/`, `/login`, `/register`, `/upload`, `/profile/settings`. - Vite dev and preview servers both use strict port `5173`, matching the backend's default `CORS_ORIGINS` and `FRONTEND_URL`.
- **SEO plugin** in `vite.config.ts`: generates `robots.txt` and `sitemap.xml` at build time. Auth-only routes, including `/encyclopedia`, are excluded from the sitemap and disallowed in `robots.txt`.
- **`lib/canvas.ts`**: patches `HTMLCanvasElement.getContext('2d')` to always pass `willReadFrequently: true` — needed for the badge card renderer in `lib/cloudBadges.ts`. - **`lib/canvas.ts`**: patches `HTMLCanvasElement.getContext('2d')` to always pass `willReadFrequently: true` — needed for the badge card renderer in `lib/cloudBadges.ts`.
## Environment Variables ## Environment Variables
+1
View File
@@ -14,6 +14,7 @@ npm run dev
``` ```
默认后端地址为 `http://localhost:8000/api/v1`。 默认后端地址为 `http://localhost:8000/api/v1`。
开发服务器和本地 Preview 都固定使用 `http://localhost:5173`,与后端默认 CORS 配置保持一致。运行 Preview 前请先停止开发服务器。
## 检查与构建 ## 检查与构建
+4 -2
View File
@@ -193,6 +193,7 @@ async function handleLogout() {
画廊 画廊
</RouterLink> </RouterLink>
<RouterLink <RouterLink
v-if="authStore.isLoggedIn"
to="/encyclopedia" to="/encyclopedia"
class="px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors" class="px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
:class="route.name === 'encyclopedia' || route.name === 'cloud-type' ? activeNavClass : inactiveNavClass" :class="route.name === 'encyclopedia' || route.name === 'cloud-type' ? activeNavClass : inactiveNavClass"
@@ -254,10 +255,10 @@ async function handleLogout() {
</div> </div>
</div> </div>
<div class="mt-3 flex items-center gap-2 text-xs text-slate-500"> <div class="mt-3 flex items-center gap-2 text-xs text-slate-500">
<span class="border border-teal-100 bg-teal-50 px-2 py-1 text-teal-700"> <!-- <span class="border border-teal-100 bg-teal-50 px-2 py-1 text-teal-700">
{{ authStore.isAdmin ? '管理员' : '观测者' }} {{ authStore.isAdmin ? '管理员' : '观测者' }}
</span> </span>
<span>OpenCloud 账号</span> <span>OpenCloud 账号</span> -->
</div> </div>
</div> </div>
@@ -356,6 +357,7 @@ async function handleLogout() {
画廊 画廊
</RouterLink> </RouterLink>
<RouterLink <RouterLink
v-if="authStore.isLoggedIn"
to="/encyclopedia" to="/encyclopedia"
class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors" class="shrink-0 px-3 py-2 text-sm font-medium tracking-[0.12em] uppercase transition-colors"
:class="route.name === 'encyclopedia' || route.name === 'cloud-type' ? activeNavClass : inactiveNavClass" :class="route.name === 'encyclopedia' || route.name === 'cloud-type' ? activeNavClass : inactiveNavClass"
+4
View File
@@ -56,8 +56,10 @@ const router = createRouter({
name: 'encyclopedia', name: 'encyclopedia',
component: () => import('@/views/encyclopedia/EncyclopediaView.vue'), component: () => import('@/views/encyclopedia/EncyclopediaView.vue'),
meta: { meta: {
requiresAuth: true,
title: '云朵图鉴', title: '云朵图鉴',
description: '浏览 10 种基础云属,了解云型特征,并通过拍摄云图点亮个人图鉴。', description: '浏览 10 种基础云属,了解云型特征,并通过拍摄云图点亮个人图鉴。',
noindex: true,
}, },
}, },
{ {
@@ -65,8 +67,10 @@ const router = createRouter({
name: 'cloud-type', name: 'cloud-type',
component: () => import('@/views/encyclopedia/CloudTypeView.vue'), component: () => import('@/views/encyclopedia/CloudTypeView.vue'),
meta: { meta: {
requiresAuth: true,
title: route => getCloudTypeName(route.params.id), title: route => getCloudTypeName(route.params.id),
description: route => `查看 ${getCloudTypeName(route.params.id)} 的识别要点、公开云图和社区观测记录。`, description: route => `查看 ${getCloudTypeName(route.params.id)} 的识别要点、公开云图和社区观测记录。`,
noindex: true,
}, },
}, },
{ {
+2
View File
@@ -6,5 +6,7 @@ declare module 'vue-router' {
description?: string | ((route: RouteLocationNormalizedLoaded) => string) description?: string | ((route: RouteLocationNormalizedLoaded) => string)
canonicalPath?: string canonicalPath?: string
noindex?: boolean noindex?: boolean
requiresAuth?: boolean
requiresAdmin?: boolean
} }
} }
+11 -11
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router' import { RouterLink, useRoute } from 'vue-router'
import { NAlert, NButton, NCard, NDropdown, NEmpty, NIcon, NSkeleton, NTag, useMessage } from 'naive-ui' import { NAlert, NButton, NCard, NDropdown, NEmpty, NIcon, NSkeleton, useMessage } from 'naive-ui'
import EncyclopediaProgressCard from '@/components/cloud/EncyclopediaProgressCard.vue' import EncyclopediaProgressCard from '@/components/cloud/EncyclopediaProgressCard.vue'
import { Settings } from '@vicons/tabler' import { Settings } from '@vicons/tabler'
import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue' import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue'
@@ -70,12 +70,12 @@ const pageTitle = computed(() => {
return isOwnProfile.value ? '我的天空日志' : `${profileData.value?.username || '这位用户'}的天空日志` return isOwnProfile.value ? '我的天空日志' : `${profileData.value?.username || '这位用户'}的天空日志`
}) })
const profileSubtitle = computed(() => { // const profileSubtitle = computed(() => {
if (isOwnProfile.value) { // if (isOwnProfile.value) {
return '回看你的云图记录、收集进度和拍摄节奏。' // return '回看你的云图记录、收集进度和拍摄节奏。'
} // }
return '公开展示的云图记录会按时间顺序陈列在这里。' // return '公开展示的云图记录会按时间顺序陈列在这里。'
}) // })
const totalShots = computed(() => clouds.value.length) const totalShots = computed(() => clouds.value.length)
@@ -526,13 +526,13 @@ watch(selectedUploadDate, async newValue => {
</button> </button>
</RouterLink> </RouterLink>
</div> </div>
<p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600"> <!-- <p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
{{ profileSubtitle }} {{ profileSubtitle }}
</p> </p> -->
<div class="mt-5 flex flex-wrap items-center gap-3 text-sm text-slate-500"> <div class="mt-5 flex flex-wrap items-center gap-3 text-sm text-slate-500">
<span>注册于 {{ formatDate(profileData?.created_at || null) }}</span> <span>注册于 {{ formatDate(profileData?.created_at || null) }}</span>
<NTag v-if="isOwnProfile" type="primary" :bordered="false">你的主页</NTag> <!-- <NTag v-if="isOwnProfile" type="primary" :bordered="false">你的主页</NTag>
<NTag v-else :bordered="false">公开展示</NTag> <NTag v-else :bordered="false">公开展示</NTag> -->
</div> </div>
</div> </div>
</div> </div>
+9 -1
View File
@@ -6,7 +6,6 @@ import tailwindcss from '@tailwindcss/vite'
const publicRoutes = [ const publicRoutes = [
'/', '/',
'/encyclopedia',
'/gallery', '/gallery',
'/community', '/community',
] ]
@@ -32,6 +31,7 @@ function seoFilesPlugin() {
'Disallow: /login', 'Disallow: /login',
'Disallow: /register', 'Disallow: /register',
'Disallow: /upload', 'Disallow: /upload',
'Disallow: /encyclopedia',
'Disallow: /profile/settings', 'Disallow: /profile/settings',
`Sitemap: ${siteUrl}/sitemap.xml`, `Sitemap: ${siteUrl}/sitemap.xml`,
'', '',
@@ -57,6 +57,14 @@ function seoFilesPlugin() {
export default defineConfig({ export default defineConfig({
plugins: [vue(), tailwindcss(), seoFilesPlugin()], plugins: [vue(), tailwindcss(), seoFilesPlugin()],
server: {
port: 5173,
strictPort: true,
},
preview: {
port: 5173,
strictPort: true,
},
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),