feat: add authentication protected encyclopedia routing, update relevant documents and configurations
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ npm run dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
默认后端地址为 `http://localhost:8000/api/v1`。
|
默认后端地址为 `http://localhost:8000/api/v1`。
|
||||||
|
开发服务器和本地 Preview 都固定使用 `http://localhost:5173`,与后端默认 CORS 配置保持一致。运行 Preview 前请先停止开发服务器。
|
||||||
|
|
||||||
## 检查与构建
|
## 检查与构建
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+2
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)),
|
||||||
|
|||||||
Reference in New Issue
Block a user