Init frontend: Vue 3 + Vite member.ose.tw

建立完整前端架構:
- 配置 Vite + Vue 3 + Element Plus + Tailwind
- 實作 API 模層(axios interceptor + Bearer/Key 認證)
- 狀態管理:auth store(用戶登入狀態)、permission store(權限快照 & Admin 認證)
- 路由守衛:/me* 需 Bearer token,/admin* 不強制
- 完成三個頁面:登入、我的資料、我的權限快照、權限 grant/revoke 管理
- 全面錯誤處理與 UI 提示(401/403/404/503 對應訊息)

Checklist 完成度:
✓ A.初始化(http.js、auth/permission store、.env)
✓ B.API 對接(/me、/me/permissions/snapshot、grant、revoke)
✓ C.頁面三組件
✓ D.行為驗證(Token 過期、自動刷新、錯誤提示)
✓ E.交付條件(獨立刷新、錯誤 UI、loading/success 狀態)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris
2026-03-29 23:26:58 +08:00
commit 3d6b04d6e5
21 changed files with 3788 additions and 0 deletions

51
src/App.vue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<div class="min-h-screen bg-gray-50">
<nav v-if="!isLoginPage" class="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-6">
<span class="font-bold text-gray-800 text-base">member.ose.tw</span>
<router-link
to="/me"
class="text-sm text-gray-600 hover:text-blue-600 transition-colors"
active-class="text-blue-600 font-medium"
>
我的資料
</router-link>
<router-link
to="/me/permissions"
class="text-sm text-gray-600 hover:text-blue-600 transition-colors"
active-class="text-blue-600 font-medium"
>
我的權限
</router-link>
<router-link
to="/admin/permissions"
class="text-sm text-gray-600 hover:text-blue-600 transition-colors"
active-class="text-blue-600 font-medium"
>
權限管理
</router-link>
</div>
<el-button v-if="authStore.isLoggedIn" size="small" @click="logout">登出</el-button>
</nav>
<main class="p-6 max-w-4xl mx-auto">
<router-view />
</main>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const isLoginPage = computed(() => route.name === 'login')
function logout() {
authStore.logout()
router.push('/login')
}
</script>