vite+vue3+ts前端搭建与配置(2)

依赖的引入与使用

1. 国际化 vue-i18n 配置

文档地址:vue-i18n

1.1 安装与locale目录结构

  • 安装

    npm install vue-i18n
    
  • locale目录结构

    ├── locale
    │   ├── lang   # 语言包
    │   │   ├── zhCn   # 中文
    │   │   │   ├── public.ts # 公共
    │   │   │   ├── menu.ts   # 菜单
    │   │   │   └── ...*.ts  # 其他
    │   │   ├── zhTw   # 繁体
    │   │   │   ├── public.ts # 公共
    │   │   │   ├── menu.ts   # 菜单
    │   │   │   └── ...*.ts  # 其他
    │   │   ├── en   # 英文
    │   │   │   └── ... # 同上
    │   │   ├── vi   # 越南
    │   │   │   └── ... # 同上
    │   │   └── ... #其他语言包
    │   └── index.ts    # 出口文件
    

    额外还需要个locale hook文件,用于方便项目中语言切换和本地存储。

1.2 基本配置

  • 语言文件示例lang/**/*.ts

    /* ======== zhCn start ======== */
    /* lang/zhCn/menu.ts */
    export default {
      home: '首页',
      system: '系统管理',
      role: '角色管理',
    }
    
    /* lang/zhCn/public .ts */
    export default {
      submit: '提交',
      cancel: '取消',
    }
    /* ======== zhCn END ======== */
    
    /* ======== en start ======== */
    /* lang/en/menu.ts */
    export default {
      home: 'Home',
      system: 'System',
      role: 'Role',
    }
    
    /* lang/en/public .ts */
    export default {
      submit: 'Submit',
      cancel: 'Cancel',
    }
    /* ======== en END ======== */
    
    /**
     * 其它语言文件夹下的文件要与 zhCn 下的文件保持一致(特别是文件名字),文件内容要一一对应。
     */
    

    一个语言文件夹(如zhCn)下有多个不同用处的文件,
    一个语言文件下一个export default对象,对象下可以包含多个键值对,键名是语言的 key,键值是语言的 value。

    在页面中就可以通过 $tt 方法获取语言的 value:{{ $t("public.confirm") }}。可以发现public就是confirm所在的文件的文件名。

  • src/locale/index.ts (重要)

    import type { App } from 'vue'
    import { createI18n } from 'vue-i18n'
    
    interface ModuleImports {
      [key: string]: any
    }
    interface localesType {
      name: string
      key: string
      path: any
    }
    
    /** 因为会在vue文件中使用,所以需要export */
    export const LOCALE_OPTIONS: localesType[] = [
      {
        name: '简体中文',
        key: 'zhCn',
        path: import.meta.glob('@/locales/lang/zhCn/*.ts', { eager: true }),
      },
      {
        name: '繁体中文',
        key: 'zhTw',
        path: import.meta.glob('@/locales/lang/zhTw/*.ts', { eager: true }),
      },
      {
        name: 'English',
        key: 'en',
        path: import.meta.glob('@/locales/lang/en/*.ts', { eager: true }),
      },
      {
        name: 'Tiếng Việt',
        key: 'vi',
        path: import.meta.glob('@/locales/lang/vi/*.ts', { eager: true }),
      },
    ]
    
    const loadModuleFnc = async (moduleImports: ModuleImports) => {
      // 过滤掉不需要的模块路径
      const modules = Object.keys(moduleImports).filter(
        (path) => path !== './index.ts'
      )
      const fetchedModules = await Promise.all(
        modules.map((path) => moduleImports[path])
      )
    
      const locale = fetchedModules.reduce((acc, module, index) => {
        const filename = modules[index].replace(/^.*\//, '').replace(/\.ts$/, '')
        if (typeof module.default === 'object') {
          acc[filename] = module.default
        }
        return acc
      }, {})
    
      return locale
    }
    
    const messages = Object.fromEntries(
      await Promise.all(
        LOCALE_OPTIONS.map(async (locale: localesType) => {
          return [locale.key, await loadModuleFnc(locale.path)]
        })
      )
    )
    
    export const i18n = createI18n({
      legacy: false,
      allowComposition: true,
      globalInjection: true,
      locale: localStorage.getItem('locale') || 'zhCn',
      fallbackLocale: 'en',
      messages,
    })
    
    export const setupI18n = (app: App) => {
      app.use(i18n)
    }
    
  • 注册 src/main.ts

    import { createApp } from 'vue'
    import App from './App.vue'
    
    // style
    import '@/styles/reset.css'
    import '@/styles/global.scss'
    
    import { setupStore } from '@/store'
    import { setupRouter } from '@/router'
    import { setupI18n } from '@/locales'
    
    async function setupApp() {
      const app = createApp(App)
    
      /** pinia */
      setupStore(app)
      setupI18n(app)
    
      /** vue router */
      await setupRouter(app)
    
      /** mount app */
      app.mount('#app')
    }
    setupApp()
    

1.3 语言切换和locale hook

  • src/hooks/locale.ts 方便项目内的切换语言的方法调用

    import { useI18n } from 'vue-i18n'
    
    export const useLocale = () => {
      const i18 = useI18n()
      const currentLocale = computed(() => {
        return i18.locale.value
      })
      const changeLocale = (value: string) => {
        if (i18.locale.value === value) {
          return
        }
        i18.locale.value = value
        localStorage.setItem('locale', value)
      }
      return {
        currentLocale,
        changeLocale,
      }
    }
    
  • 语言切换组件示例src/layout/components/LangSwitch.vue

    <template>
      <el-dropdown class="mr-3" @command="handleCommand">
        <SvgIcon
          name="languageIcon"
          :color="'var(--auo-c1)'"
          class="text-3xl cursor-pointer"
        />
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item
              v-for="lang in LOCALE_OPTIONS"
              :command="lang.key"
              >{{ lang.name }}</el-dropdown-item
            >
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </template>
    
    <script setup lang="ts">
    import { LOCALE_OPTIONS } from '@/locales/index'
    import { useLocale } from '@/hooks/locale'
    
    const { changeLocale } = useLocale()
    
    const handleCommand = (command: string) => {
      changeLocale(command)
    }
    </script>
    

1.4 Element Plus国际化设置

<template>
  <el-config-provider :locale="locale">
    <router-view />
  </el-config-provider>
</template>

<script setup lang="ts">
import { useLocale } from '@/hooks/locale'
import { zhTw, zhCn, en } from 'element-plus/es/locales.mjs'

const { currentLocale } = useLocale()
const locale = computed(() => {
  switch (currentLocale.value) {
    case 'zhCn':
      return zhCn
    case 'zhTw':
      return zhTw
    default:
      return en
  }
})
</script>

2. 路由 route

2.1 安装与目录结构

  • 安装

    pnpm add vue-router
    
  • 目录结构

    ├── router
    │   ├── guard   # 路由守卫
    │   │   ├── permission.ts
    │   │   └── index.ts
    │   ├── helpers # 路由辅助方法
    │   │   ├── orders.ts
    │   │   └── utils.ts
    │   ├── modules  # 路由配置
    │   │   ├── home.ts
    │   │   └── ...*.ts
    │   ├── index.ts      # 出口文件
    │   └── types.d.ts    # 类型定义
    

2.2 路由配置-类型声明

文件位置:src/router/types.d.ts

import type { RouteComponent, RouteRecordRaw } from 'vue-router'
import type { FunctionalComponent, defineComponent } from 'vue'

declare module 'vue-router' {
  interface RouteMeta {
    /** 路由标题(可用来作document.title或者菜单的名称) */
    title: string
    /** 用来支持多国语言 locale 与 title 同时存在时 优先使用 locale */
    locale?: string
    /** 菜单图标 `可选` */
    icon?: string | FunctionalComponent
    /** 控制有权访问该页面的角色 */
    roles?: string[]
    /** 如果为 true,则需要登录才能访问当前页面 */
    requiresAuth?: boolean
    /** 是否在菜单中隐藏(一些列表、表格的详情页面需要通过参数跳转,所以不能显示在菜单中) */
    hide?: boolean
    /** 路由顺序,可用于菜单的排序(值越高,越向前) */
    order?: number
    /** 如果为 true 则不会固定到tab-bar */
    noAffix?: boolean
    /** 如果为 true 则不会缓存页面 */
    ignoreCache?: boolean
  }
}

declare global {
  type AppRouteRecordRaw = RouteRecordRaw & {
    meta?: RouteMeta
    children?: AppRouteRecordRaw[]
    props?: Boolean | Recordable | ((to: RouteLocationNormalized) => Recordable)
    component?: RouteComponent | string
  }
}

2.3 路由配置-modules路由文件示例

各路由对应一个文件,文件位置:src/router/modules/*.ts

  • 路由文件示例 1 home.ts

    import { HOME } from '@/router/helpers/orders'
    
    export default {
      path: '/home',
      name: 'Home',
      component: () => import('@/views/home/index.vue'),
      meta: {
        title: 'home',
        locale: 'menu.home',
        icon: 'IconHome',
        order: HOME,
      },
    } satisfies AppRouteRecordRaw
    
  • 路由文件示例 2 system.ts

    import { SYSTEM } from '@/router/helpers/orders'
    
    export default {
      path: '/system',
      name: 'System',
      redirect: '/system/dept',
      meta: {
        title: 'System',
        locale: 'menu.system',
        icon: 'IconSystem',
        order: SYSTEM,
      },
      children: [
        {
          path: '/dept',
          name: 'Dept',
          component: () => import('@/views/system/dept/index.vue'),
          meta: {
            title: 'Dept',
            locale: 'menu.dept',
            icon: 'IconDept',
          },
        },
        {
          path: '/role',
          name: 'Role',
          component: () => import('@/views/system/role/index.vue'),
          meta: {
            title: 'Role',
            locale: 'menu.role',
            icon: 'IconRole',
          },
        },
        {
          path: '/user',
          name: 'User',
          component: () => import('@/views/system/user/index.vue'),
          meta: {
            title: 'User',
            locale: 'menu.user',
            icon: 'IconUser',
          },
        },
      ],
    } satisfies AppRouteRecordRaw
    

2.4 helpers文件存放一些路由辅助方法

  1. order.ts: 维护路由显示顺序,控制菜单排序
// src/router/helpers/order.ts
/**
 * 维护路由显示顺序,控制菜单排序
 */
export const HOME = 1,
  SYSTEM = 10
  1. utils.ts: 处理路由时的一些函数
// src/router/helpers/order.ts
/**
 * 权限路由排序
 * @param routes - 权限路由
 */
export function sortRoutes(routes: AppRouteRecordRaw[]) {
  return routes
    .sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order))
    .map((i) => {
      if (i.children) sortRoutes(i.children)
      return i
    })
}

/**
 * 处理全部导入的路由模块
 * @param modules - 路由模块
 */
export function formatModuleRoutes(
  modules: Record<string, { default: AppRouteRecordRaw }>
) {
  const result: AppRouteRecordRaw[] = []

  Object.keys(modules).forEach((key: any) => {
    const item = modules[key]
    if (!item) return
    const moduleList = Array.isArray(item) ? [...item] : [item]
    result.push(...moduleList)
  })
  return sortRoutes(result)
}
  1. route-listener.ts: 路由监听
// src/router/helpers/route-listener.ts
/**
 * Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management
 * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
 */
import mitt, { type Handler } from 'mitt'
import type { RouteLocationNormalized } from 'vue-router'

const emitter = mitt()

const key = Symbol('ROUTE_CHANGE')

let latestRoute: RouteLocationNormalized

export function setRouteEmitter(to: RouteLocationNormalized) {
  emitter.emit(key, to)
  latestRoute = to
}

export function listenerRouteChange(
  handler: (route: RouteLocationNormalized) => void,
  immediate = true
) {
  emitter.on(key, handler as Handler)
  if (immediate && latestRoute) {
    handler(latestRoute)
  }
}

export function removeRouteListener() {
  emitter.off(key)
}

2.5 guard 文件夹

  • src/router/guard/index.ts

    import type { Router } from 'vue-router'
    import { setRouteEmitter } from '@/router/helpers/route-listener'
    import { setupPermissionGuard } from '@/router/guard/permission'
    export const createRouterGuards = (router: Router) => {
      router.beforeEach(async (to) => {
        // emit route change
        setRouteEmitter(to)
      })
    
      // 路由跳转 权限控制
      setupPermissionGuard(router)
    }
    
  • src/router/guard/permission.ts

    import type { Router } from 'vue-router'
    import NProgress from 'nprogress' //  nprogress 注意要在src/main.ts中引入nprogress样式import "nprogress/nprogress.css";
    
    export const setupPermissionGuard = (router: Router) => {
      router.beforeEach(async (to, from, next) => {
        NProgress.start()
        console.log('setupPermissionGuard_to', to)
        console.log('setupPermissionGuard_from', from)
    
        /**
         * 此处 可做权限判断
         * 1. 判断用户是否登录,没有登录则 next({ path: '/login', query: { redirect: to.fullPath, ...to.query } })
         *    注意,要注意排除login页面的登录跳转,不然会死循环。
         * 2. 判断用户是否有当前页的访问权限,有则 next(),没有则 ElementUI.Message.error('无权访问此页面!')
         */
    
        next()
        NProgress.done()
      })
    }
    

2.6 创建路由

import type { App } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { formatModuleRoutes } from './helpers/utils'
import { createRouterGuards } from './guard/index'

const DEFAULT_LAYOUT = () => import('@/layout/index.vue')

/**
 * 匹配 src/router/modules 目录, 自动导入全部静态路由
 * 如何匹配所有文件请看:https://github.com/mrmlnc/fast-glob#basic-syntax
 * 如何排除文件请看:https://cn.vitejs.dev/guide/features.html#negative-patterns
 */
const modules = import.meta.glob<{ default: AppRouteRecordRaw }>(
  './modules/*.ts',
  { eager: true, import: 'default' }
)

export const appRoutes: AppRouteRecordRaw[] = formatModuleRoutes(modules)

/** 创建路由实例 */
export const router = createRouter({
  // 路由模式hash
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      name: 'root',
      redirect: '/home',
      component: DEFAULT_LAYOUT,
      children: appRoutes,
    },
    {
      path: '/:pathMatch(.*)*',
      redirect: '/home',
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('@/views/login/index.vue'),
    },
  ],
  scrollBehavior: (to, from, savedPosition) => {
    // 优先使用浏览器保存的滚动位置
    if (savedPosition) return savedPosition

    // 处理hash锚点定位
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth',
      }
    }

    // 相同路由不同参数时保持滚动距离
    if (to.name === from.name && to.path === from.path) {
      return false
    }

    return {
      top: 0,
      left: 0,
      behavior: 'auto', // 快速定位时保持原生滚动效果
    }
  },
})

/** setup vue router. - [安装vue路由] */
export const setupRouter = async (app: App) => {
  app.use(router)

  // 创建路由守卫
  createRouterGuards(router)

  // 路由准备就绪后挂载APP实例
  await router.isReady()
}

2.7 在main.ts中注册router

文件位置:src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import { setupRouter } from '@/router'

async function setupApp() {
  const app = createApp(App)

  /** vue router */
  await setupRouter(app)

  /** mount app */
  app.mount('#app')
}
setupApp()

2.8 Other

(示例)路由列表渲染 menu.vue

<template>
  <el-menu
    :default-active="activeIndex"
    :router="true"
    :ellipsis="false"
    class="el-menu-demo"
    background-color="#545c64"
    text-color="#ffffff"
    active-text-color="#ffd04b"
  >
    <template v-for="item in menuList">
      <el-sub-menu
        v-if="item.children && item.children.length > 0"
        :index="item.path"
      >
        <template #title>{{ returnLocale(item) }}</template>
        <el-menu-item
          v-for="(subItem, sIdx) in item.children"
          :key="sIdx"
          :index="subItem.path"
          :teleported="true"
          :disabled="IsrequiresAuth(subItem)"
          >{{ returnLocale(subItem) }}</el-menu-item
        >
      </el-sub-menu>

      <el-menu-item
        v-else
        :index="item.path"
        :disabled="IsrequiresAuth(item)"
        >{{ item!.meta!.title }}</el-menu-item
      >
    </template>
  </el-menu>
</template>

<script lang="ts" setup>
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
const route = useRoute()
const activeIndex = ref<string>('')

const menuList = computed(() => {
  const routes = route.matched[0].children as AppRouteRecordRaw[]
  return routes
})

const IsrequiresAuth = (item: AppRouteRecordRaw) => {
  // 具体权限判断在路由拦截器中
  return item.meta?.requiresAuth
}

const returnLocale = (item: AppRouteRecordRaw) => {
  if (item.meta?.locale) return t(item.meta.locale)
  return item.meta?.title
}
</script>

3. axios配置

Axios地址
当前内容为基础使用方式,可进一步优化及封装.

3.1 安装与目录结构

  • 安装

    pnpm add axios
    # or
    npm install axios
    
  • 目录结构

    service
    ├── api # 各模块的 APi
    │ ├── user.ts # 用户模块的 API
    │ └── ...*.ts # 其他模块
    └── request.ts # 请求封装
    

3.2 请求封装request

// src/service/request.ts
import axios from 'axios';
import type { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus';
import { router } from "@/router/index";

/**
 * 创建请求
 * @param baseURL - 接口地址
 */
function createRequest(baseURL: string) {
  const instance = axios.create({
    baseURL,
    timeout: 1000 * 60,
    withCredentials: true,  // 允许跨域请求携带凭据
  })

  /** 请求拦截器 */
  instance.interceptors.request.use(
    (config) => {
      // 在发送请求之前做些什么

      // 请求头中添加token
      if (config.url?.includes('/login')) {
        return config;
      }

      const tokenStr = localStorage.getItem('Token');
      if(tokenStr){
        const parsedToken = JSON.parse(tokenStr);
        parsedToken?.token && (config.headers.Authorization = `Bearer ${parsedToken.token}`);
      }
      
      return config;
    },
    (error: AxiosError): Promise<AxiosError> => {
      return Promise.reject(error)
    }
  )

  /** 响应拦截器 */
  instance.interceptors.response.use(
    (response: AxiosResponse) => {
      // 在响应返回后,可以进行一些处理,例如 刷新Token 等等。

      // 如果是文件流,则直接返回
      if (
        response.config.responseType === 'blob' ||
        response.config.responseType === 'arraybuffer'
      ) {
        return response
      }

      // 处理空响应
      if (!response.data) {
        return Promise.reject(new Error('Empty response'))
      }

      // 统一返回数据,注意,这里需要根据你的接口返回数据结构进行修改。
      // 若你的接口的data中没有code,则需要判断response.status === 200
      if (response.data.code === 200) {
        return response.data
      } else {
        ElMessage.error(response.data.msg ?? '请求失败')
        return Promise.reject(new Error(response.data.msg))
      }
    },
    (error: AxiosError): Promise<AxiosError> => {
      // 处理响应错误
      /**
       * 处理响应错误,判断是否有响应(服务器返回了错误)
       * 比如:网路错误;超时错误;请求不成功的错误
       */
      if (error.response) {
        const { status, data } = error.response as AxiosResponse;

        let errorMessage = "";
        switch (status) {
          case 400:
            errorMessage = "请求错误";
            break;
          case 401:
            errorMessage = "登录已过期,请重新登录";
            router.replace({ path: "/login" });
            break;
          case 403:
            errorMessage = "拒绝访问";
            break;
          case 404:
            errorMessage = "请求的资源不存在";
            break;
          case 408:
            errorMessage = "请求超时";
            break;
          case 500:
            errorMessage = "服务器内部错误,请稍后重试";
            break;
          default:
            errorMessage = data?.msg || `服务器返回错误,状态码:${status}`;
        }

        ElMessage.error(data?.msg || errorMessage || error.message);

      } else if (error.request) { // 判断是否是网络错误
        ElMessage.error("网络错误,请检查您的网络连接");
      } else { // 其他错误(如配置错误等)
        ElMessage.error(error.message || "请求发生未知错误");
      }

      return Promise.reject(error);
    }
  )

  return instance
}

/** 导出地址,供项目内可能存在的使用的地方 */
export const BASEURL = import.meta.env.VITE_APP_BASE_URL;

/** 真实后台数据接口 */
export const instance: AxiosInstance = createRequest(BASEURL)

/** mock 数据接口 */
export const mockInstance: AxiosInstance = createRequest('/mock')

3.3 全局声明请求类型type.d.ts

文件:src/service/type.d.ts

// 请求返回数据类型 (Tips:与后台开发确认返回格式)
interface ApiResponse<T = unknown> {
  code: number;
  msg: string;
  data: T;
  fail: boolean;
  success: boolean;
}

// 带分页的表格的返回数据类型
interface PaginationRecord<T = Record<string, any>> {
  records: T[];
  size: number;
  pages: number;
  current: number;
  total: number;
}

3.3 请求文件示例/api/*.ts

src/service/api/*.ts

模块接口请求文件示例:

// src/service/api/user.ts
import { instance } from '../request'

/**
 * 登录
 * @param userName - 用户名
 * @param password - 密码
 */
interface LoginData {
  password: string
  username: string
}
interface LoginRes {
  access_token: string
  expires_in: string
  refresh_token: string
}
export const fetchLogin = (data: LoginData): Promise<ApiResponse<LoginRes>> => {
  return instance.post('/api/login', data)
}

/** 获取用户信息 */
interface UserInfo {
  userName: string
  userId: string
  roles: string[]
  [key: string]: any
}
export const fetchUserInfo = (): Promise<ApiResponse<UserInfo>> => {
  return mockRequest.get('/getUserInfo')
}

// ---------------------
export interface SiteDTOInfo {
  createTime: string;
  createUser: string;
  lmUser: string;
  lmTime: string;
  description: string;
  id: string;
  site: string;
}
export interface FormDataParam {
  createUser?: string
  keyword?: string
  site?: string;
}
export const siteList = (data: FormDataParam = {}): Promise<ApiResponse<SiteDTOInfo[]>> => {
  return instance.post(`/api/site/all`, data)
}

4. 状态管理 pinia

4.1 安装与目录结构

  • 安装

    pnpm add pinia pinia-plugin-persistedstate
    
  • 目录结构

    store
    ├── modules # 各模块对应的 store
    │ ├── user.ts
    │ └── ...\*.ts # 其他模块
    └── index.ts
    

4.2 配置 pinia

  • src/store/index.ts

    import type { App } from 'vue';
    import { createPinia } from 'pinia';
    // https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
    import piniaPluginPersist from 'pinia-plugin-persistedstate'; // 持久化存储
    
    /** setup vue store plugin: pinia. - [安装vue状态管理插件:pinia] */
    export function setupStore(app: App) {
      const pinia = createPinia()
      pinia.use(piniaPluginPersist)
    
      app.use(pinia)
    }
    
  • src/main.ts

    import { createApp } from 'vue'
    import App from './App.vue'
    import { setupStore } from '@/store'
    
    async function setupApp() {
      const app = createApp(App)
      /** vue store */
      setupStore(app)
      app.mount('#app')
    }
    setupApp()
    

4.3 pinia的使用示例以及持久化

// src/stores/login.ts
import { defineStore } from "pinia";
import { login, logout, refreshToken as refreshTokenApi, getUserInfo, type LoginParam } from "@/api/modules/user";
import { ElMessage } from "element-plus";
import { router } from "@/router/index";

export default defineStore("userStore", () => {
  const userInfo = ref<Record<string, any>>({
    name: ''
  });
  const token = ref<string>('');
  const refreshToken = ref<string>('');
  const loading = ref<boolean>(false);

  const setLoading = (value: boolean) => {
    loading.value = value;
  }

  const fetchLogin = async (param: LoginParam) => {
    setLoading(true);
    try {
      const { data } = await login(param);
      setToken(data);
      ElMessage.success('登录成功');
      router.push('/');
    } finally {
      setLoading(false);
    }
  };

  const fetchLogout = async () => {
    setLoading(true);
    try {
      await logout();
      ElMessage.success('退出成功');
      router.push('/login');
    } finally {
      setLoading(false);
    }
  };

  const setToken = (data: Record<string, string>) => {
    const { access_token, refresh_token } = data;
    token.value = access_token;
    refreshToken.value = refresh_token;
  }

  const fetchUserInfo = async () => {
    const res = await getUserInfo();
    userInfo.value = res.data
  };

  const fetchRefreshToken = async () => {
    const res = await refreshTokenApi(refreshToken.value);
    setToken(res.data);
  };

  return {
    userInfo,
    token,
    refreshToken,

    loading,
    setLoading,

    fetchLogin,
    fetchLogout,
    fetchUserInfo,
    fetchRefreshToken
  };
}, {
  // 配置:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/config.html
  // persist: true
  persist: [
    { key: 'userInfo', storage: localStorage, pick: ['userInfo'] },
    { key: 'Token', storage: localStorage, pick: ['token', 'refreshToken'] }
  ]
});

5. CSS 原子化 tailwindcss

5.1 安装与引用

  • 安装

    npm install tailwindcss @tailwindcss/vite
    
  • 配置Vite.Config.ts

    // vite.config.ts  <==> config/plugins/index.ts
    import type { PluginOption } from 'vite'
    import tailwindcss from '@tailwindcss/vite'
    /**
     * vite插件
     * @returns PluginOption[]
     */
    export const setupVitePlugins = (): PluginOption[] => {
      const plugins = [tailwindcss()]
    
      return plugins
    }
    
  • 导入Tailwind CSS

    styles 文件夹下创建该文件(tailwind.css),并再 main.ts 文件中导入。

    @import 'tailwindcss';
    

    然后即可使用 Tailwind CSS

5.2 自定义样式

Theme variables

1. 使用示例
@import 'tailwindcss';

@font-face {
  /** 字体文件存于 public/fonts/* */
  font-family: 'Harmony Sans';
  src: url('/fonts/HarmonyOSSansSC.ttf') format('truetype');
}
@font-face {
  font-family: 'Harmony Medium';
  src: url('/fonts/HarmonyOSMedium.ttf') format('truetype');
}
@font-face {
  font-family: 'Harmony Bold';
  src: url('/fonts/HarmonyOSBold.ttf') format('truetype');
}

@theme {
  --color-midnight: #121063; /* Example:text-midnight / bg-bermuda / text-mint-500 */
  --color-bermuda: #78dcca;
  --color-mint-500: oklch(0.72 0.11 178);

  --spacing-11: 111px; /* Example: h-11 / w-11 / size-11 */

  --font-HarmonySans: 'Harmony Sans';
  --font-HarmonyMedium: 'Harmony Medium'; /* Example: font-HarmonyMedium */
  --font-HarmonyBold: 'Harmony Bold';
}

Tips: 发现指令提示警告 Unknown at rule @theme css(unknownAtRules),其实是 vscode 无法识别 @theme 指令,不影响使用。
解决方式: 修改vscode配置文件:"css.lint.unknownAtRules": "ignore"

2. Tailwind CSS 主题变量命名空间

主题变量在命名空间中定义,每个命名空间对应一个或多个实用工具类或变体 API。

在这些命名空间中定义新的主题变量,将使对应的新实用工具和变体在您的项目中可用:

Namespace对应的实用工具类Utility classes
--color-*颜色相关工具类bg-red-500, text-sky-300
--font-*字体家族工具类font-sans, font-mono
--text-*字体大小工具类text-xl, text-2xl
--font-weight-*字体粗细工具类font-bold, font-light
--tracking-*字母间距工具类tracking-wide, tracking-tight
--leading-*行高工具类leading-tight, leading-loose
--breakpoint-*响应式断点变体sm:text-lg, md:hidden
--container-*容器查询变体@sm:flex, max-w-md
--spacing-*间距和尺寸工具类px-4, max-h-16, mt-8
--radius-*圆角工具类rounded-sm, rounded-full
--shadow-*盒阴影工具类shadow-md, shadow-lg
--inset-shadow-*内阴影工具类inset-shadow-xs
--drop-shadow-*投影滤镜工具类drop-shadow-md
--blur-*模糊滤镜工具类blur-md, blur-sm
--perspective-*透视变换工具类perspective-near
--aspect-*宽高比工具类aspect-video, aspect-square
--ease-*过渡时序函数工具类ease-out, ease-in
--animate-*动画工具类animate-spin, animate-pulse

使用建议

  1. 自定义主题时,可以在@theme规则中扩展这些命名空间
  2. 所有自定义变量需要遵循--{namespace}-*的命名规范
  3. 工具类名称与变量命名保持语义化关联

6. 使用iconify图标

https://blog.youkuaiyun.com/weixin_46872121/article/details/138212930

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值