feat(auth): 添加用户认证模块与登录页面
This commit is contained in:
2
auto-imports.d.ts
vendored
2
auto-imports.d.ts
vendored
@@ -156,6 +156,7 @@ declare global {
|
|||||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useAuthStore: typeof import('./src/stores/auth-store-auto-imports')['useAuthStore']
|
||||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||||
@@ -499,6 +500,7 @@ declare module 'vue' {
|
|||||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
|
readonly useAuthStore: UnwrapRef<typeof import('./src/stores/auth-store-auto-imports')['useAuthStore']>
|
||||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
|
|||||||
<Toast />
|
<Toast />
|
||||||
|
|
||||||
<AppNaiveUIProvider>
|
<AppNaiveUIProvider>
|
||||||
<RouterView />
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
</AppNaiveUIProvider>
|
</AppNaiveUIProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
|
import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
|
||||||
import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
|
import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
|
||||||
import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
||||||
|
import UserDropdown from './components/UserDropdown.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,6 +12,7 @@ import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<LanguageSwitchButton />
|
<LanguageSwitchButton />
|
||||||
<ThemeSwitchButton />
|
<ThemeSwitchButton />
|
||||||
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useAuthStore();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
const options = computed(() => [
|
||||||
|
{
|
||||||
|
label: userStore.userInfo?.nickname || userStore.userInfo?.username || '用户',
|
||||||
|
key: 'user',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: 'logout',
|
||||||
|
icon: () => <icon-material-symbols-logout class="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleSelect(key: string) {
|
||||||
|
if (key === 'logout') {
|
||||||
|
dialog.warning({
|
||||||
|
title: '退出登录',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
userStore.clearToken('用户退出登录');
|
||||||
|
router.push('/login');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDropdown :options="options" placement="bottom-end" @select="handleSelect">
|
||||||
|
<NButton quaternary circle>
|
||||||
|
<icon-material-symbols-account-circle w-5 h-5 />
|
||||||
|
</NButton>
|
||||||
|
</NDropdown>
|
||||||
|
</template>
|
||||||
@@ -49,14 +49,4 @@ const appStore = useAppStore();
|
|||||||
#__SCROLL_EL_ID__ {
|
#__SCROLL_EL_ID__ {
|
||||||
@include scrollbar;
|
@include scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.25s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import './styles/index.ts';
|
|||||||
import { LogLevels } from 'consola';
|
import { LogLevels } from 'consola';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { setupPlugins } from './plugins';
|
import { setupPlugins } from './plugins';
|
||||||
import { router } from './plugins/00.router-plugin.ts';
|
|
||||||
|
|
||||||
consola.level = LogLevels.verbose;
|
consola.level = LogLevels.verbose;
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
setupPlugins(app);
|
setupPlugins(app);
|
||||||
await router.isReady();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 280));
|
await new Promise((resolve) => setTimeout(resolve, 280));
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1,5 +1,87 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
definePage({ meta: { ignoreAuth: true, layout: false } });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useAuthStore();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const formValue = ref({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!formValue.value.username || !formValue.value.password) {
|
||||||
|
message.warning('请输入用户名和密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await userStore.login(formValue.value.username, formValue.value.password);
|
||||||
|
if (result.success) {
|
||||||
|
message.success('登录成功');
|
||||||
|
router.push('/');
|
||||||
|
} else {
|
||||||
|
message.error(result.message || '登录失败');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('登录异常');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>Login</div>
|
<div class="login-page">
|
||||||
|
<NCard class="login-card" title="用户登录">
|
||||||
|
<NForm :model="formValue" label-placement="left" label-width="80">
|
||||||
|
<NFormItem label="用户名" path="username">
|
||||||
|
<NInput
|
||||||
|
v-model:value="formValue.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="密码" path="password">
|
||||||
|
<NInput
|
||||||
|
v-model:value="formValue.password"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem :show-label="false">
|
||||||
|
<NButton type="primary" block :loading="loading" @click="handleLogin"> 登录 </NButton>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<div class="login-hint">
|
||||||
|
<NText depth="3">提示:用户名和密码均为 admin</NText>
|
||||||
|
</div>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hint {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
|
|||||||
import { setupLayouts } from 'virtual:meta-layouts';
|
import { setupLayouts } from 'virtual:meta-layouts';
|
||||||
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
|
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import type { RouteNamedMap } from 'vue-router/auto-routes';
|
import type { Router } from 'vue-router';
|
||||||
import { routes, handleHotUpdate } from 'vue-router/auto-routes';
|
import { handleHotUpdate, routes } from 'vue-router/auto-routes';
|
||||||
|
|
||||||
const setupLayoutsResult = setupLayouts(routes);
|
const setupLayoutsResult = setupLayouts(routes);
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -15,6 +15,10 @@ const router = createRouter({
|
|||||||
strict: true,
|
strict: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.isReady().then(() => {
|
||||||
|
console.debug('✅ [router is ready]');
|
||||||
|
});
|
||||||
|
|
||||||
router.onError((error) => {
|
router.onError((error) => {
|
||||||
console.debug('🚨 [router error]:', error);
|
console.debug('🚨 [router error]:', error);
|
||||||
});
|
});
|
||||||
@@ -33,49 +37,24 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
|||||||
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
||||||
createNProgressGuard(router);
|
createNProgressGuard(router);
|
||||||
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
|
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
|
||||||
Object.assign(globalThis, { stack: createStackGuard(router) });
|
Object.assign(window, { stack: createStackGuard(router) });
|
||||||
|
|
||||||
|
// >>>
|
||||||
|
Object.values(
|
||||||
|
import.meta.glob<{
|
||||||
|
createGuard?: (router: Router) => void;
|
||||||
|
}>('./router-guard/*.ts', { eager: true /* true 为同步,false 为异步 */ }),
|
||||||
|
).forEach((module) => {
|
||||||
|
module.createGuard?.(router);
|
||||||
|
});
|
||||||
|
// <<<
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue-router' {
|
if (__DEV__) Object.assign(window, { router });
|
||||||
/* definePage({ meta: { title: '示例演示' } }); */
|
|
||||||
interface RouteMeta {
|
|
||||||
/**
|
|
||||||
* @description 是否在菜单中隐藏
|
|
||||||
*/
|
|
||||||
hideInMenu?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 菜单标题 //!⚠️通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
|
|
||||||
*/
|
|
||||||
title?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 使用的布局,设置为 false 则表示不使用布局
|
|
||||||
*/
|
|
||||||
layout?: string | false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 菜单项是否渲染为可点击链接,默认为 true
|
|
||||||
* - true: 使用 RouterLink 包装,可点击跳转
|
|
||||||
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
|
|
||||||
*/
|
|
||||||
link?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
|
|
||||||
*/
|
|
||||||
order?: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { router, setupLayoutsResult };
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__DEV__) Object.assign(globalThis, { router });
|
|
||||||
// This will update routes at runtime without reloading the page
|
// This will update routes at runtime without reloading the page
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
handleHotUpdate(router);
|
handleHotUpdate(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { router, setupLayoutsResult };
|
||||||
|
|||||||
42
src/plugins/00.router-plugin.types.ts
Normal file
42
src/plugins/00.router-plugin.types.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { RouteNamedMap } from 'vue-router/auto-routes';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
/* definePage({ meta: { title: '示例演示' } }); */
|
||||||
|
interface RouteMeta {
|
||||||
|
/**
|
||||||
|
* @description 是否在菜单中隐藏
|
||||||
|
*/
|
||||||
|
hideInMenu?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 菜单标题 //!⚠️通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 使用的布局,设置为 false 则表示不使用布局
|
||||||
|
*/
|
||||||
|
layout?: string | false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 菜单项是否渲染为可点击链接,默认为 true
|
||||||
|
* - true: 使用 RouterLink 包装,可点击跳转
|
||||||
|
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
|
||||||
|
*/
|
||||||
|
link?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
|
||||||
|
*/
|
||||||
|
order?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 是否忽略权限,默认为 false
|
||||||
|
*/
|
||||||
|
ignoreAuth?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@ type UserPlugin = (ctx: UserPluginContext) => void;
|
|||||||
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
|
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
|
||||||
type UserPluginContext = { app: import('vue').App<Element> };
|
type UserPluginContext = { app: import('vue').App<Element> };
|
||||||
|
|
||||||
const autoInstallModules: AutoInstallModule = import.meta.glob('./!(index).ts', {
|
const autoInstallModules: AutoInstallModule = import.meta.glob(
|
||||||
|
['./*.ts', '!./**/*.types.ts', '!./index.ts'],
|
||||||
|
{
|
||||||
eager: true /* true 为同步,false 为异步 */,
|
eager: true /* true 为同步,false 为异步 */,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export function setupPlugins(app: import('vue').App) {
|
export function setupPlugins(app: import('vue').App) {
|
||||||
console.group('🔌 Plugins');
|
console.group(`🔌 Installing ${Object.keys(autoInstallModules).length} plugins`);
|
||||||
for (const path in autoInstallModules) {
|
for (const path in autoInstallModules) {
|
||||||
const module = autoInstallModules[path] as AutoInstallModule;
|
const module = autoInstallModules[path] as AutoInstallModule;
|
||||||
if (module.install) {
|
if (module.install) {
|
||||||
|
|||||||
28
src/plugins/router-guard/router-permission-guard.ts
Normal file
28
src/plugins/router-guard/router-permission-guard.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
export function createGuard(router: Router) {
|
||||||
|
router.beforeEach(async (to /* , from */) => {
|
||||||
|
const userStore = useAuthStore();
|
||||||
|
|
||||||
|
if (to.name === 'Login') {
|
||||||
|
userStore.clearToken('User navigated to login page');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to.meta.ignoreAuth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
console.debug('🔑 [permission-guard] 用户未登录,重定向到登录页');
|
||||||
|
return { name: 'Login' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.beforeResolve(async (/* to, from */) => {
|
||||||
|
const userStore = useAuthStore();
|
||||||
|
if (userStore.isLoggedIn && !userStore.userInfo) {
|
||||||
|
console.debug('🔑 [permission-guard] 用户信息不存在,尝试获取用户信息');
|
||||||
|
await userStore.fetchUserInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
45
src/stores/auth-store-auto-imports.ts
Normal file
45
src/stores/auth-store-auto-imports.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const token = useLocalStorage<string | null>('auth-token', null);
|
||||||
|
const userInfo = ref<Record<string, any> | null>(null);
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value);
|
||||||
|
|
||||||
|
function clearToken(reason?: string) {
|
||||||
|
consola.info('🚮 [auth-store] clear: ', reason);
|
||||||
|
token.value = null;
|
||||||
|
userInfo.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(username: string, password: string) {
|
||||||
|
// 模拟登录延迟
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// 模拟验证
|
||||||
|
if (username === 'admin' && password === 'admin') {
|
||||||
|
token.value = `mock-token-${Date.now()}`;
|
||||||
|
await fetchUserInfo();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: '用户名或密码错误' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
if (!token.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟获取用户信息延迟
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// 模拟从服务器获取用户信息
|
||||||
|
userInfo.value = {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
nickname: '管理员',
|
||||||
|
roles: ['admin'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token, isLoggedIn, userInfo, clearToken, login, fetchUserInfo };
|
||||||
|
});
|
||||||
9
src/styles/css/transition.css
Normal file
9
src/styles/css/transition.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
||||||
|
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
import './reset-primevue.css';
|
|
||||||
|
import './css/reset-primevue.css';
|
||||||
|
import './css/transition.css';
|
||||||
|
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
|
|||||||
Reference in New Issue
Block a user