<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      nprogress插件的安裝和使用,登錄業務的代碼編寫,統一設置token,處理頁面訪問登錄狀態和登錄過期。pinia-plugin-persistedstate插件的安裝和使用。

      nprogress插件的安裝和使用,登錄業務的代碼編寫,統一設置token,處理頁面訪問登錄狀態和登錄過期。pinia-plugin-persistedstate插件的安裝和使用。

      npm install nprogress
      npm i --save-dev @types/nprogress
      

      新建services/user.ts mkdir src/services && touch src/services/user.ts

      /**
       * 用戶相關API & 業務邏輯(service層)
       * - 只負責異步API、數據轉換、復雜流程
       * - 類型全部解耦,只用import type { UserInfo } from ...
       */
      
      import http from '@/utils/http'
      import type { UserInfo } from '@/store/types/user'
      
      /**
       * 用戶登錄
       * @param username 用戶名
       * @param password 密碼
       * @returns 用戶信息
       */
      export function loginApi(username: string, password: string) {
        return http.post<UserInfo>('/api/login', { username, password })
      }
      
      /**
       * 獲取當前用戶信息
       */
      export function fetchUserApi() {
        return http.get<UserInfo>('/api/user')
      }
      
      /**
       * 用戶登出
       */
      export function logoutApi() {
        return http.post<void>('/api/logout')
      }
      
      

      對應的store層代碼

      /**
       * 用戶Pinia Store(只做狀態與同步變更)
       * - Google標準:全部類型解耦,最大注釋
       * - 只做本地狀態管理/變更/展示,異步交由service
       */
      
      import { defineStore } from 'pinia'
      import type { UserInfo } from '../types/user'
      import { loginApi, fetchUserApi, logoutApi } from '@/services/user'
      
      export const useUserStore = defineStore('user', {
        /**
         * 1. State:本地用戶信息
         */
        state: (): UserInfo => ({
          id: '',
          name: '訪客',
          email: '',
          avatarUrl: '',
          isLoggedIn: false
        }),
      
        /**
         * 2. Actions:同步變更+異步業務(推薦復雜業務分離出service層)
         */
        actions: {
          /**
           * 本地登錄變更(僅存數據,不調接口)
           * @param info 登錄成功后的用戶信息
           */
          setLogin(info: Omit<UserInfo, 'isLoggedIn'>) {
            this.id = info.id
            this.name = info.name
            this.email = info.email
            this.avatarUrl = info.avatarUrl ?? ''
            this.isLoggedIn = true
          },
      
          /**
           * 本地登出變更(重置所有狀態)
           */
          setLogout() {
            this.id = ''
            this.name = '訪客'
            this.email = ''
            this.avatarUrl = ''
            this.isLoggedIn = false
          },
      
          /**
           * 登錄流程(調API + 本地寫入)
           */
          async login(username: string, password: string) {
            const user = await loginApi(username, password)
            this.setLogin(user)
            // 可選:本地存儲token等
          },
      
          /**
           * 拉取當前用戶信息并寫入本地
           */
          async fetchUser() {
            const user = await fetchUserApi()
            this.setLogin(user)
          },
      
          /**
           * 登出流程(調API + 本地清理)
           */
          async logout() {
            await logoutApi()
            this.setLogout()
            // 可選:清理token
          }
        },
      
        /**
         * 3. Getters:只做本地展示/狀態派生
         */
        getters: {
          /**
           * 用戶名首字母大寫
           */
          displayName: (state): string =>
            state.name ? state.name.charAt(0).toUpperCase() + state.name.slice(1) : '訪客'
        }
          persist: true // 開啟自動持久化,所有state字段自動同步到本地
      
      })
      
      

      layout層代碼

      mkdir src/layouts && touch src/layouts/MainLayout.vue && touch src/layouts/AuthLayout.vue

      <template>
        <!-- 主應用外殼,包含頭部、側邊欄、主內容和全局通知等 -->
        <div class="main-layout">
          <!-- 頭部導航欄(可插槽自定義) -->
          <AppHeader />
          <div class="layout-body">
            <!-- 側邊欄(如有權限菜單) -->
            <AppSidebar />
            <!-- 主內容區:用slot注入實際頁面內容 -->
            <main class="main-content">
              <slot />
            </main>
          </div>
          <!-- 全局底部 -->
          <AppFooter />
        </div>
      </template>
      
      <script setup lang="ts">
      // 引用復用組件
      import AppHeader from '@/components/AppHeader.vue'
      import AppSidebar from '@/components/AppSidebar.vue'
      import AppFooter from '@/components/AppFooter.vue'
      </script>
      
      <style scoped>
      .main-layout {
        min-height: 100vh;
        display: flex;
        flex-direction: column;
        background: #f5f6fa;
      }
      .layout-body {
        flex: 1;
        display: flex;
        flex-direction: row;
      }
      .main-content {
        flex: 1;
        padding: 24px;
        background: #fff;
        min-height: 0;
        overflow: auto;
      }
      </style>
      
      
      <template>
        <!-- 登錄/注冊/忘記密碼專用外殼,居中簡約 -->
        <div class="auth-layout">
          <div class="auth-box">
            <slot />
          </div>
        </div>
      </template>
      
      <script setup lang="ts">
      /**
       * AuthLayout
       * 用于登錄/注冊等不需要主導航的簡約頁面外殼
       */
      </script>
      
      <style scoped>
      .auth-layout {
        min-height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        background: linear-gradient(120deg, #409eff 0%, #f5f6fa 100%);
      }
      .auth-box {
        width: 350px;
        padding: 32px 24px;
        background: #fff;
        border-radius: 18px;
        box-shadow: 0 2px 16px #409eff33;
      }
      </style>
      
      

      對應componets層代碼

      mkdir src/components && touch src/components/AppHeader.vue && touch src/components/AppSidebar.vue && touch src/components/AppFooter.vue && touch src/components/UserAvatar.vue

      <template>
        <header class="app-header">
          <!-- 應用LOGO -->
          <div class="logo">MyApp</div>
          <!-- 右側用戶欄 -->
          <div class="header-actions">
            <UserAvatar />
            <!-- 更多操作按鈕 -->
          </div>
        </header>
      </template>
      
      <script setup lang="ts">
      import UserAvatar from './UserAvatar.vue'
      </script>
      
      <style scoped>
      .app-header {
        height: 60px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 0 32px;
        background: #409eff;
        color: #fff;
      }
      .logo {
        font-size: 22px;
        font-weight: bold;
        letter-spacing: 2px;
      }
      .header-actions {
        display: flex;
        align-items: center;
        gap: 20px;
      }
      </style>
      
      
      <template>
        <aside class="app-sidebar">
          <!-- 側邊菜單欄,可根據權限/路由動態生成 -->
          <nav>
            <ul>
              <li><router-link to="/">首頁</router-link></li>
              <li><router-link to="/about">關于</router-link></li>
              <!-- 可擴展更多 -->
            </ul>
          </nav>
        </aside>
      </template>
      
      <script setup lang="ts">
      /**
       * AppSidebar
       * 側邊菜單,可接入動態權限/路由管理
       */
      </script>
      
      <style scoped>
      .app-sidebar {
        width: 200px;
        background: #fff;
        border-right: 1px solid #eee;
        padding-top: 18px;
        min-height: 100%;
      }
      .app-sidebar ul {
        list-style: none;
        padding: 0;
      }
      .app-sidebar li {
        margin: 12px 0;
      }
      .app-sidebar a {
        color: #333;
        text-decoration: none;
        padding: 8px 16px;
        display: block;
        border-radius: 8px;
      }
      .app-sidebar a.router-link-active {
        background: #409eff22;
        color: #409eff;
      }
      </style>
      
      
      <template>
        <footer class="app-footer">
          ? 2025 MyApp. All rights reserved.
        </footer>
      </template>
      
      <script setup lang="ts">
      /**
       * AppFooter
       * 全局底部信息欄
       */
      </script>
      
      <style scoped>
      .app-footer {
        height: 48px;
        text-align: center;
        color: #888;
        background: #fafbfc;
        line-height: 48px;
        font-size: 14px;
      }
      </style>
      
      
      <template>
        <div class="user-avatar">
          <img :src="avatarUrl" :alt="userName" />
          <span class="user-name">{{ userName }}</span>
        </div>
      </template>
      
      <script setup lang="ts">
      import { computed } from 'vue'
      import { useUserStore } from '@/store/modules/user'
      
      // 讀取當前用戶信息
      const userStore = useUserStore()
      const avatarUrl = computed(() => userStore.avatarUrl || 'https://api.dicebear.com/8.x/pixel-art/svg?seed=user')
      const userName = computed(() => userStore.displayName)
      </script>
      
      <style scoped>
      .user-avatar {
        display: flex;
        align-items: center;
        gap: 8px;
      }
      .user-avatar img {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        object-fit: cover;
      }
      .user-name {
        font-size: 15px;
        color: #333;
      }
      </style>
      
      

      mkdir src/router && mkdir src/router/modules && touch src/router/index.ts && touch src/router/modules/other.ts && touch src/router/modules/base.ts

      /**
       * 路由主入口(router/index.ts)
       * - 自動聚合所有模塊路由
       * - 全局守衛:nprogress、鑒權、動態layout等
       */
      
      import { createRouter, createWebHistory } from 'vue-router'
      import NProgress from 'nprogress'
      import 'nprogress/nprogress.css'
      import { baseRoutes } from './modules/base'
      import { otherRoutes } from './modules/other'
      import { useUserStore } from '@/store/modules/user'
      
      // 合并所有路由
      const routes = [...baseRoutes, ...otherRoutes]
      
      const router = createRouter({
        history: createWebHistory(),
        routes
      })
      
      // 全局前置守衛:進度條 + 登錄攔截
      router.beforeEach((to, _from, next) => {
        NProgress.start()
        const userStore = useUserStore()
        if (to.meta.requiresAuth && !userStore.isLoggedIn) {
          next('/login')
        } else {
          next()
        }
      })
      
      router.afterEach(() => {
        NProgress.done()
      })
      
      export default router
      
      
      /**
       * 其它業務模塊路由(router/modules/other.ts)
       */
      
      import type { RouteRecordRaw } from 'vue-router'
      
      export const otherRoutes: RouteRecordRaw[] = [
        {
          path: '/about',
          name: 'About',
          component: () => import('@/views/About.vue'),
          meta: { layout: 'main', requiresAuth: true, title: '關于我們' }
        }
      ]
      
      
      /**
       * 基礎路由模塊(router/modules/base.ts)
       * - 管理無需權限的公共頁面路由
       */
      
      import type { RouteRecordRaw } from 'vue-router'
      
      export const baseRoutes: RouteRecordRaw[] = [
        {
          path: '/login',
          name: 'Login',
          component: () => import('@/views/Login.vue'),
          meta: { layout: 'auth', public: true, title: '登錄' }
        },
        {
          path: '/',
          name: 'Home',
          component: () => import('@/views/Home.vue'),
          meta: { layout: 'main', requiresAuth: true, title: '首頁' }
        }
        // 可擴展更多公共頁面
      ]
      
      

      mkdir src/views && touch src/views/AppHome.vue && touch src/views/AppLogin.vue && touch src/views/AppAbout.vue

      <template>
        <MainLayout>
          <h1>歡迎,{{ userStore.displayName }}</h1>
          <p>這是首頁內容。</p>
        </MainLayout>
      </template>
      
      <script setup lang="ts">
      // 引入主layout
      import MainLayout from '@/layouts/MainLayout.vue'
      import { useUserStore } from '@/store/modules/user'
      
      // 讀取用戶信息
      const userStore = useUserStore()
      </script>
      
      <!-- 樣式可選 -->
      
      
      <template>
        <AuthLayout>
          <form class="login-form" @submit.prevent="handleLogin">
            <h2>登錄</h2>
            <input v-model="username" placeholder="用戶名" />
            <input v-model="password" type="password" placeholder="密碼" />
            <button type="submit">登錄</button>
            <div class="error" v-if="errorMsg">{{ errorMsg }}</div>
          </form>
        </AuthLayout>
      </template>
      
      <script setup lang="ts">
      import { ref } from 'vue'
      import AuthLayout from '@/layouts/AuthLayout.vue'
      import { useUserStore } from '@/store/modules/user'
      
      const userStore = useUserStore()
      const username = ref('')
      const password = ref('')
      const errorMsg = ref('')
      
      const handleLogin = async () => {
        try {
          await userStore.login(username.value, password.value)
          window.location.href = '/'
        } catch  {
          errorMsg.value = '用戶名或密碼錯誤'
        }
      }
      </script>
      
      <style scoped>
      .login-form {
        display: flex;
        flex-direction: column;
        gap: 18px;
      }
      .error {
        color: red;
        margin-top: 8px;
      }
      </style>
      
      
      <template>
        <MainLayout>
          <h1>關于我們</h1>
          <p>本項目遵循Google級代碼分層規范。</p>
        </MainLayout>
      </template>
      
      <script setup lang="ts">
      import MainLayout from '@/layouts/MainLayout.vue'
      </script>
      
      

      修改src/main.ts

      /**
       * 應用主入口(main.ts)
       * - 注冊Pinia、Router、ElementPlus等
       * - 最大化注釋,團隊友好
       */
      
      import { createApp } from 'vue'
      import App from './App.vue'
      import router from '@/router'
      import { pinia } from '@/store'
      import ElementPlus from 'element-plus'
      import 'element-plus/dist/index.css'
      
      
      // 創建vue app
      const app = createApp(App)
      
      // 注冊全局插件
      app.use(pinia)
      app.use(router)
      app.use(ElementPlus)
      
      // 掛載應用
      app.mount('#app')
      
      

      懶得寫后端接口,postman可以定義偽接口。參照:http://www.rzrgm.cn/surpassme/p/16489009.html

      修改.env

      VITE_API_BASE_URL=/
      VITE_APP_TITLE=My Vite App
      

      看數據變化的插件,Vue.js devtools

      安裝pinia-plugin-persistedstate

      npm install pinia-plugin-persistedstate
      
      

      修改pinia單例文件

      // 引入并創建Pinia實例,便于主入口集成
      import { createPinia } from 'pinia'
      import piniaPersist from 'pinia-plugin-persistedstate'
      
      // 導出Pinia實例
      export const pinia = createPinia()
      pinia.use(piniaPersist)
      
      

      token過期,修改https

      import axios  from 'axios'
      import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
      import router from '@/router' // 若用SPA跳轉,否則可用location.href
      import { useUserStore } from '@/store/modules/user'
      
      const http = axios.create({
        baseURL: import.meta.env.VITE_API_BASE_URL,
        timeout: 10000,
        headers: { 'Content-Type': 'application/json' }
      })
      
      http.interceptors.request.use(
        config => config,
        error => Promise.reject(error)
      )
      
      http.interceptors.response.use(
        (response: AxiosResponse) => {
          if (response.data && typeof response.data === 'object' && 'data' in response.data) {
            return response.data.data
          }
          return response.data
        },
        (error: AxiosError) => {
          // === 統一處理token過期/失效 ===
          // 1. 如果是http層401
          if (error.response && error.response.status === 401) {
            // 2. 清理用戶store,自動登出
            const userStore = useUserStore()
            userStore.setLogout?.()
            // 3. 跳轉登錄頁,防止死循環/多次重定向用replace
            router.replace('/login')
            // 或 window.location.href = '/login'
          }
          // 4. 也可以根據后端返回自定義錯誤碼處理
          // if (error.response && error.response.data && error.response.data.code === 'TOKEN_EXPIRED') {
          //   ...
          // }
          return Promise.reject(error)
        })
      
      // 重點是這一行的泛型
      const get = <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
        return http.get<unknown, T>(url, config)
      }
      
      const post = <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => {
        return http.post<unknown, T>(url, data, config)
      }
      
      export default { get, post }
       
      
      posted @ 2025-05-27 13:59  $Traitor$  閱讀(51)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产精品流白浆无遮挡 | 十八禁午夜福利免费网站| 国产jlzzjlzz视频免费看| 亚洲十八禁一区二区三区| 一二三四免费中文字幕| 成年女人黄小视频| 亚洲人成日韩中文字幕不卡| 国产精品视频一区二区不卡| 99久久精品国产一区二区| 色综合 图片区 小说区| 一区二区丝袜美腿视频| 亚洲人妻一区二区精品| 成人精品日韩专区在线观看| 国产成人精品性色av麻豆| 日本美女性亚洲精品黄色| 国产老肥熟一区二区三区| 四虎精品永久在线视频| 亚洲精品一区| 国产亚洲精品一区二区无| 久播影院无码中文字幕| 亚洲高清成人av在线| 亚洲一级片一区二区三区| 精品无码国产日韩制服丝袜| 特黄aaaaaaaaa毛片免费视频| 四虎成人免费视频在线播放| 真实单亲乱l仑对白视频| 欧美国产日产一区二区| 国产高颜值极品嫩模视频| 日韩精品国产二区三区| 狠狠综合久久综合88亚洲| 欧美寡妇xxxx黑人猛交| 正在播放肥臀熟妇在线视频| 国产综合久久亚洲综合| 国内精品久久久久影院网站| 国产福利酱国产一区二区 | 97人妻精品一区二区三区| 久久综合亚洲色一区二区三区| 又大又粗又爽的少妇免费视频| 色伊人久久综合中文字幕| 国产精品VA尤物在线观看| 国产成人无码综合亚洲日韩|