iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
影片教學

Nuxt 3 快速入門系列 第 26

[影片教學] Nuxt 3 路由頁面的權限判斷 - 實戰部落格系列 Part 4

  • 分享至 

  • xImage
  •  

Yes

👆建議你可以使用影片子母畫面功能或全螢幕播放來獲得最佳的觀賞體驗,👇下方是本篇教學的相關筆記。


建立路由中間件

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('/')
    }
  }
})

建立刪除文章按鈕與串接 API

調整 ./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 的夥伴。

範例程式碼


上一篇
[影片教學] Nuxt 3 Server API 權限判斷與串接 - 實戰部落格系列 Part 3
下一篇
[影片教學] Nuxt 3 SEO 搜尋引擎最佳化 - 實戰部落格系列 Part 5
系列文
Nuxt 3 快速入門30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
大叔
iT邦新手 5 級 ‧ 2023-10-29 17:05:47

在 server/api/articles 目錄下建立一個檔案 ./server/api/articles/[id].path.js:
這邊的[id].path.js檔名好像寫錯了

Ryan iT邦新手 2 級 ‧ 2023-10-30 14:23:19 檢舉

感謝提醒,已修正
/images/emoticon/emoticon02.gif

我要留言

立即登入留言