👆建議你可以使用影片子母畫面功能或全螢幕播放來獲得最佳的觀賞體驗,👇下方是本篇教學的相關筆記。
在 middleware 目錄下建立一個檔案 ./middleware/auth.js:
export default defineNuxtRouteMiddleware(async () => {
const user = await $fetch('/api/whoami', {
headers: useRequestHeaders(['cookie'])
}).catch((error) => {
console.error(error)
return null
})
if (user?.id !== 1) {
return navigateTo('/')
}
})
調整 ./pages/articles/[id].vue 檔案:
<script setup>
// ...
definePageMeta({
middleware: 'auth'
})
</script>
調整 ./components/LayoutHeader.vue 檔案,調整 script 的部分:
<template>
<!-- ... -->
</template>
<script setup>
const { data: userInfo } = await useFetch('/api/whoami')
const { data } = await useFetch('/api/whoami')
const userInfo = useState('userInfo')
watch(
data,
(newData) => {
userInfo.value = newData
},
{
immediate: true
}
)
const handleLogout = () => {
$fetch('/api/session', {
method: 'DELETE'
}).then(() => {
userInfo.value = null
})
}
</script>
調整 ./middleware/auth.js 檔案:
export default defineNuxtRouteMiddleware(async () => {
const userInfo = useState('userInfo')
if (userInfo.value) {
if (userInfo.value?.id !== 1) {
return navigateTo('/')
}
} else {
const user = await $fetch('/api/whoami', {
headers: useRequestHeaders(['cookie'])
}).catch((error) => {
console.error(error)
return null
})
if (user?.id !== 1) {
return navigateTo('/')
}
}
})
調整 ./pages/articles/[id].vue 檔案:
<template>
<div class="flex w-full justify-center px-6 lg:px-0">
<div v-if="pending">
<Icon class="h-6 w-6 text-gray-500" name="eos-icons:loading" />
</div>
<template v-else>
<div v-if="error" class="my-4">
<span class="text-gray-500">發生了一點錯誤,請稍後再嘗試</span>
<p class="my-2 text-rose-500">{{ error }}</p>
</div>
<div v-else-if="article" class="mb-8 flex w-full flex-col justify-center md:max-w-3xl">
<div class="mt-4 flex justify-center">
<img :src="article.cover" />
</div>
<div class="my-2 flex flex-col justify-between sm:my-0 sm:flex-row sm:items-center">
<time class="my-2 text-sm text-gray-400">
{{ new Date(article.updated_at).toLocaleString() }}
</time>
<div v-if="userInfo?.id === 1" class="flex-rowx flex gap-3">
<button
class="flex items-center text-sm text-gray-400 hover:font-semibold hover:text-rose-500"
@click="handleDeleteArticle"
>
<Icon class="mr-1 h-4 w-4" name="ri:delete-bin-line" />
刪除
</button>
</div>
</div>
<h1 class="break-words text-4xl font-semibold text-gray-700">{{ article.title }}</h1>
<div class="mt-6 break-words">
{{ article.content }}
</div>
</div>
</template>
</div>
</template>
<script setup>
const route = useRoute()
const { pending, data: article, error } = await useFetch(`/api/articles/${route.params.id}`)
const userInfo = useState('userInfo')
const handleDeleteArticle = () => {
const answer = confirm('你確定要刪除文章嗎?')
if (answer) {
$fetch(`/api/articles/${route.params.id}`, {
method: 'DELETE'
})
.then(() => {
navigateTo('/')
})
.catch((error) => {
console.error(error)
alert(error)
})
}
}
</script>
在 server/api/articles 目錄下建立一個檔案 ./server/api/articles/[id].patch.js:
import { pool } from '@/server/utils/db'
export default defineEventHandler(async (event) => {
if (event.context?.auth?.user?.id !== 1) {
throw createError({
statusCode: 401,
message: '沒有權限'
})
}
const articleId = await getRouterParam(event, 'id')
const body = await readBody(event)
const articleRecord = await pool
.query(
'UPDATE "article" SET "title" = $1, "content" = $2, "cover" = $3, "updated_at" = NOW() WHERE "id" = $4 RETURNING *;',
[body.title, body.content, body.cover, articleId]
)
.then((result) => {
if (result.rowCount === 1) {
return result.rows?.[0]
}
})
.catch((error) => {
console.error(error)
throw createError({
statusCode: 500,
message: '無法更新文章,請稍候再試'
})
})
if (!articleRecord) {
throw createError({
statusCode: 400,
message: '更新文章失敗,請稍候再試'
})
}
return articleRecord
})
在 pages/articles 目錄下建立一個檔案 ./pages/articles/edit.vue:
<template>
<div class="flex w-full justify-center px-6 lg:px-0">
<div class="mb-8 flex w-full max-w-3xl justify-center">
<form class="w-full space-y-8 divide-y divide-gray-200" @submit.prevent="handleSubmit">
<div class="space-y-8 divide-y divide-gray-200">
<div>
<div class="mt-6">
<h3 class="text-xl font-medium leading-6 text-gray-900">更新文章</h3>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-6">
<div class="col-span-12">
<label for="title" class="block text-sm font-medium text-gray-700">
文章標題
</label>
<div class="mt-1">
<input
id="title"
v-model="articleData.title"
placeholder="請撰輸入文章標題"
name="title"
type="text"
autocomplete="title"
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-emerald-500 focus:outline-none focus:ring-emerald-500 sm:text-sm"
/>
</div>
</div>
<div class="col-span-12">
<label for="cover" class="block text-sm font-medium text-gray-700">
代表性圖片連結
</label>
<div class="mt-1">
<input
id="cover"
v-model="articleData.cover"
placeholder="請撰輸入網址連結"
name="cover"
type="text"
autocomplete="cover"
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-emerald-500 focus:outline-none focus:ring-emerald-500 sm:text-sm"
/>
</div>
</div>
<div class="col-span-12">
<label for="about" class="block text-sm font-medium text-gray-700">文章內容</label>
<div class="mt-1">
<textarea
id="content"
v-model="articleData.content"
name="content"
rows="4"
placeholder="請撰寫文章內容..."
class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-emerald-500 focus:outline-none focus:ring-emerald-500 sm:text-sm"
/>
</div>
</div>
</div>
</div>
</div>
<div class="pt-5">
<div class="flex justify-end">
<NuxtLink
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
to="/"
>
取消
</NuxtLink>
<button
type="submit"
class="ml-3 inline-flex justify-center rounded-md border border-transparent bg-emerald-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
>
更新
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { data: articleData } = await useFetch(`/api/articles/${route.query.id}`)
const handleSubmit = async () => {
await $fetch(`/api/articles/${route.query.id}`, {
method: 'PATCH',
body: {
title: articleData.value.title,
content: articleData.value.content,
cover: articleData.value.cover
}
})
.then((response) => {
navigateTo({
name: 'articles-id',
params: {
id: response.id
}
})
})
.catch((error) => alert(error))
}
definePageMeta({
middleware: 'auth'
})
</script>
調整 ./pages/articles/[id].vue 檔案,在瀏覽文章頁面中添加編輯的按鈕:
<NuxtLink
class="flex items-center text-sm text-gray-400 hover:font-semibold hover:text-emerald-500"
:to="{
name: 'articles-edit',
query: {
id: route.params.id
}
}"
>
<Icon class="mr-1 h-4 w-4" name="ri:edit-line" />
編輯
</NuxtLink>
調整 ./components/LayoutHeader.vue 檔案,在登出後添加 navigateTo('/') 重新導向回首頁:
const handleLogout = () => {
$fetch('/api/session', {
method: 'DELETE'
}).then(() => {
userInfo.value = null
navigateTo('/')
})
}
感謝大家的閱讀,歡迎大家給予建議與討論,也請各位大大鞭小力一些:)
如果對這個 Nuxt 3 系列感興趣,可以訂閱
接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。
範例程式碼
在 server/api/articles 目錄下建立一個檔案 ./server/api/articles/[id].path.js:
這邊的[id].path.js檔名好像寫錯了
感謝提醒,已修正