依赖的引入与使用
1. 国际化 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。在页面中就可以通过
$t或t方法获取语言的 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.tsimport { 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.tsimport { 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.tsimport { 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文件存放一些路由辅助方法
- 如
order.ts: 维护路由显示顺序,控制菜单排序
// src/router/helpers/order.ts
/**
* 维护路由显示顺序,控制菜单排序
*/
export const HOME = 1,
SYSTEM = 10
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)
}
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.tsimport 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.tsimport 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.tsimport 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.tsimport { 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 自定义样式
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 |
使用建议
- 自定义主题时,可以在
@theme规则中扩展这些命名空间 - 所有自定义变量需要遵循
--{namespace}-*的命名规范 - 工具类名称与变量命名保持语义化关联
6. 使用iconify图标
https://blog.youkuaiyun.com/weixin_46872121/article/details/138212930
1029

被折叠的 条评论
为什么被折叠?



