├── src ├── assets │ ├── images │ │ └── .gitkeep │ ├── styles │ │ ├── index.scss │ │ └── transition │ │ │ └── fade.scss │ └── svg │ │ └── vue.svg ├── hooks │ ├── business │ │ └── .gitkeep │ └── common │ │ ├── useLoading.ts │ │ ├── useBoolean.ts │ │ └── useReload.ts ├── components │ ├── business │ │ └── .gitkeep │ ├── common │ │ ├── NaiveProviderContent.vue │ │ └── NaiveProvider.vue │ └── HelloWorld.vue ├── store │ ├── index.ts │ └── modules │ │ ├── setting.ts │ │ └── route.ts ├── App.vue ├── views │ ├── example │ │ ├── KeepAlive.vue │ │ ├── UnKeep.vue │ │ ├── AboutAuthor.vue │ │ ├── AboutProject.vue │ │ └── HomePage.vue │ └── redirect │ │ └── index.vue ├── main.ts ├── settings │ └── index.ts ├── layouts │ ├── components │ │ ├── LayoutFooter.vue │ │ ├── LayoutLogo.vue │ │ ├── LayoutContent.vue │ │ ├── LayoutMenu.vue │ │ └── LayoutHeader.vue │ └── PcLayout.vue ├── utils │ ├── index.ts │ ├── cipher.ts │ ├── router.ts │ ├── http │ │ ├── axiosCancel.ts │ │ └── index.ts │ ├── is.ts │ └── storage │ │ └── index.ts └── router │ ├── modules │ ├── constant.ts │ └── example.ts │ ├── index.ts │ └── guard.ts ├── .prettierignore ├── typings ├── vite-env.d.ts ├── window.d.ts ├── components.d.ts └── auto-imports.d.ts ├── commitlint.config.cjs ├── vercel.json ├── .env.production ├── .env.development ├── tsconfig.node.json ├── .editorconfig ├── .gitignore ├── prettier.config.js ├── public ├── loading.svg ├── loading.css └── vite.svg ├── tsconfig.json ├── index.html ├── eslint.config.mjs ├── .cz-config.cjs ├── package.json ├── vite.config.ts ├── uno.config.ts ├── README.md └── .eslintrc-auto-import.json /src/assets/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/business/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/business/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pnpm-lock.yaml 3 | dist 4 | -------------------------------------------------------------------------------- /typings/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './transition/fade.scss'; 2 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/:path*", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | export const store = createPinia() 4 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 项目根目录 2 | VITE_BASE_URL = ./ 3 | 4 | # api接口地址 5 | VITE_API_URL = /api 6 | 7 | # 是否打开打包可视化 8 | VISUALIZER = false 9 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 项目根目录 2 | VITE_BASE_URL = ./ 3 | 4 | # api接口代理 5 | VITE_PROXY = http://localhost:8199 6 | 7 | # api接口地址 8 | VITE_API_URL = /api 9 | 10 | # 是否打开打包可视化 11 | VISUALIZER = true 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /src/views/example/KeepAlive.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/views/example/UnKeep.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/hooks/common/useLoading.ts: -------------------------------------------------------------------------------- 1 | import { useBoolean } from './useBoolean' 2 | 3 | export function useLoading(initValue = false) { 4 | const { 5 | bool: loading, 6 | setTrue: startLoading, 7 | setFalse: endLoading, 8 | } = useBoolean(initValue) 9 | 10 | return { 11 | loading, 12 | startLoading, 13 | endLoading, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/store/modules/setting.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { animationType } from '@/settings' 3 | 4 | export const useSettingStore = defineStore({ 5 | id: 'setting', 6 | state: () => ({ 7 | darkTheme: false, 8 | animationType, 9 | }), 10 | getters: { 11 | getDarkTheme(): boolean { 12 | return this.darkTheme 13 | }, 14 | }, 15 | actions: {}, 16 | }) 17 | -------------------------------------------------------------------------------- /typings/window.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DialogProviderInst, 3 | LoadingBarProviderInst, 4 | MessageProviderInst, 5 | NotificationProviderInst, 6 | } from 'naive-ui' 7 | 8 | declare global { 9 | interface Window { 10 | $loading: LoadingBarProviderInst 11 | $dialog: DialogProviderInst 12 | $message: MessageProviderInst 13 | $notification: NotificationProviderInst 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'uno.css' 2 | // 统一浏览器默认样式 3 | import '@unocss/reset/normalize.css' 4 | import '@/assets/styles/index.scss' 5 | import { createApp } from 'vue' 6 | import App from './App.vue' 7 | import { store } from '@/store' 8 | import { routerPlugin } from '@/router' 9 | 10 | /** 11 | * 加载页面,可以在mount的前后执行任务 12 | */ 13 | function startApp() { 14 | createApp(App).use(store).use(routerPlugin).mount('#app') 15 | } 16 | 17 | startApp() 18 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | // 默认缓存期限为7天 2 | export const defaultCacheTime = 60 * 60 * 24 * 7 3 | 4 | // storage是否加密,默认使用AES-ECB 5 | export const storageEncrypt = true 6 | 7 | // aes-ecb加密密钥 8 | export const encryptoKey = 'KgAh3Tc7PCL1j794' 9 | 10 | // api接口url 11 | export const apiBaseUrl = import.meta.env.VITE_API_URL 12 | 13 | // 请求超时时长 14 | export const requestTimeout = 5000 15 | 16 | // 路由动画类型 17 | export const animationType = 'zoom-fade' 18 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutFooter.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 16 | -------------------------------------------------------------------------------- /src/assets/svg/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { Icon } from '@iconify/vue' 3 | 4 | /** 5 | * 动态渲染iconify 6 | * @param icon - 图标名称 7 | * @param color - 图标颜色 8 | * @param size - 图标大小 9 | */ 10 | export function iconifyRender(icon: string, color?: string, size?: number) { 11 | const style: { color?: string, size?: string } = {} 12 | if (color) 13 | style.color = color 14 | 15 | if (size) 16 | style.size = `${size}px` 17 | 18 | return () => h(Icon, { icon, style }) 19 | } 20 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 字符串使用单引号 3 | singleQuote: true, 4 | // 每行末尾自动添加分号 5 | semi: false, 6 | // tab缩进大小,默认为2 7 | tabWidth: 2, 8 | // 使用tab缩进,默认false 9 | useTabs: false, 10 | // 对象中打印空格 默认true 11 | // true: { foo: bar } 12 | // false: {foo: bar} 13 | bracketSpacing: true, 14 | // 箭头函数参数括号 默认avoid 可选 avoid| always 15 | // avoid 能省略括号的时候就省略 例如x => x 16 | // always 总是有括号 17 | arrowParens: 'avoid', 18 | // 换行长度,默认80 19 | printWidth: 80, 20 | 21 | jsxBracketSameLine: true, 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/common/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export function useBoolean(initValue = false) { 4 | const bool = ref(initValue) 5 | 6 | function setBool(value: boolean) { 7 | bool.value = value 8 | } 9 | function setTrue() { 10 | setBool(true) 11 | } 12 | function setFalse() { 13 | setBool(false) 14 | } 15 | function toggle() { 16 | setBool(!bool.value) 17 | } 18 | 19 | return { 20 | bool, 21 | setBool, 22 | setTrue, 23 | setFalse, 24 | toggle, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/common/NaiveProviderContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/views/example/AboutAuthor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 34 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /src/views/example/AboutProject.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 34 | -------------------------------------------------------------------------------- /src/hooks/common/useReload.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | import { useBoolean } from './useBoolean' 3 | 4 | /** 重载 */ 5 | export function useReload() { 6 | // 重载的标志 7 | const { bool: reloadFlag, setTrue, setFalse } = useBoolean(true) 8 | 9 | /** 10 | * 触发重载 11 | * @param duration - 延迟时间(ms) 12 | */ 13 | async function handleReload(duration = 0) { 14 | setFalse() 15 | await nextTick() 16 | if (duration) { 17 | setTimeout(() => { 18 | setTrue() 19 | }, duration) 20 | } 21 | else { 22 | setTrue() 23 | } 24 | } 25 | 26 | return { 27 | reloadFlag, 28 | handleReload, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutLogo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /src/router/modules/constant.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import layout from '../../layouts/PcLayout.vue' 3 | 4 | const routes: Array = [ 5 | { 6 | path: '/redirect', 7 | name: 'RedirectComponent', 8 | component: layout, 9 | meta: { 10 | hidden: true, 11 | hideBreadcrumb: true, 12 | }, 13 | children: [ 14 | { 15 | path: '/redirect/:path(.*)', 16 | name: 'RedirectComponent', 17 | component: () => import('@/views/redirect/index.vue'), 18 | }, 19 | ], 20 | }, 21 | { 22 | path: '/', 23 | name: 'Root', 24 | redirect: '/home', 25 | }, 26 | ] 27 | 28 | export default routes 29 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["ESNext", "DOM"], 6 | "useDefineForClassFields": true, 7 | "baseUrl": ".", 8 | "module": "ESNext", 9 | "moduleResolution": "Node", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "types": ["unplugin-icons/types/vue"], 15 | "strict": true, 16 | "noEmit": true, 17 | "esModuleInterop": true, 18 | "isolatedModules": true, 19 | "skipLibCheck": true 20 | }, 21 | "references": [{ "path": "./tsconfig.node.json" }], 22 | "include": [ 23 | "typings", 24 | "src/**/*.ts", 25 | "src/**/*.d.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /public/loading.css: -------------------------------------------------------------------------------- 1 | .before-create-app-loading-container { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | .before-create-app-loading-logo { 14 | margin-bottom: 10px; 15 | } 16 | 17 | .before-create-app-loading-logo:hover { 18 | filter: drop-shadow(0 0 2em #42b883aa) drop-shadow(0 0 4em #42b883aa); 19 | } 20 | 21 | .before-create-app-loading-title { 22 | font-size: 28px; 23 | font-weight: 500; 24 | color: #646464; 25 | } 26 | 27 | .before-create-app-loading-svg { 28 | --colorA: #42b883aa; 29 | filter: drop-shadow(0 0 2px var(--colorA)) drop-shadow(0 0 5px var(--colorA)) 30 | drop-shadow(0 0 10px var(--colorA)) drop-shadow(0 0 15px var(--colorA)); 31 | } 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + Vue + TS 9 | 10 | 11 |
12 |
13 | 19 |

Vue3 PCWeb Starter

20 | 21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | ignores: ['*.sh', '**/*.sh/**', 'node_modules', '**/node_modules/**', 'lib', '**/lib/**', '*.md', '**/*.md/**', '*.woff', '**/*.woff/**', '*.ttf', '**/*.ttf/**', '.vscode', '**/.vscode/**', '.idea', '**/.idea/**', 'dist/', 'dist/**/', 'public', 'public/**', 'docs', 'docs/**', '.local', '**/.local/**', '!.env-config.ts', '!**/.env-config.ts/**', 'components.d.ts', '**/components.d.ts/**'], 5 | formatters: { 6 | prettierOptions: { 7 | // 字符串使用单引号 8 | singleQuote: true, 9 | // 每行末尾自动添加分号 10 | semi: false, 11 | // tab缩进大小,默认为2 12 | tabWidth: 2, 13 | // 使用tab缩进,默认false 14 | useTabs: false, 15 | // 对象中打印空格 默认true 16 | // true: { foo: bar } 17 | // false: {foo: bar} 18 | bracketSpacing: true, 19 | // 箭头函数参数括号 默认avoid 可选 avoid| always 20 | // avoid 能省略括号的时候就省略 例如x => x 21 | // always 总是有括号 22 | arrowParens: 'avoid', 23 | // 换行长度,默认80 24 | printWidth: 80, 25 | jsxBracketSameLine: true, 26 | }, 27 | }, 28 | unocss: true, 29 | vue: true, 30 | }) 31 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vue' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import { createGuard } from './guard' 5 | 6 | type RouteGlob = { 7 | [key in string]: { 8 | default: Array 9 | } 10 | } 11 | 12 | // 从modules目录动态导入路由 13 | const modules = import.meta.glob('./modules/**/*.ts', { 14 | eager: true, 15 | }) as RouteGlob 16 | const routeModuleList: RouteRecordRaw[] = [] 17 | 18 | Object.keys(modules).forEach((key) => { 19 | const module = modules[key].default || {} 20 | const modList = Array.isArray(module) ? [...module] : [module] 21 | routeModuleList.push(...modList) 22 | }) 23 | 24 | // 导出默认静态路由 25 | export const constantRoute: RouteRecordRaw[] = [...routeModuleList] 26 | 27 | // 创建路由实例 28 | const router = createRouter({ 29 | history: createWebHistory(import.meta.env.VITE_BASE_URL as string), 30 | routes: routeModuleList, 31 | }) 32 | 33 | export const routerPlugin = { 34 | install(app) { 35 | app.use(router) 36 | createGuard(router) 37 | }, 38 | } as Plugin 39 | 40 | // 也可以使用导出的路由实例进行挂载 41 | export default router 42 | -------------------------------------------------------------------------------- /src/store/modules/route.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import type { RouteRecordRaw } from 'vue-router' 4 | import { constantRoute } from '@/router' 5 | 6 | export interface IRouteState { 7 | keepAliveComponents: string[] 8 | routes: RouteRecordRaw[] 9 | reloadFlag: boolean 10 | } 11 | 12 | export const useRouteStore = defineStore({ 13 | id: 'route', 14 | state: (): IRouteState => ({ 15 | keepAliveComponents: [], 16 | routes: constantRoute, 17 | reloadFlag: true, 18 | }), 19 | actions: { 20 | setKeepAliveComponents(compNames: string[]) { 21 | // 设置需要缓存的组件 22 | this.keepAliveComponents = compNames 23 | }, 24 | /** 25 | * 重载页面 26 | * @param duration - 重载的延迟时间(ms) 27 | */ 28 | async reloadPage(duration = 0) { 29 | this.reloadFlag = false 30 | await nextTick() 31 | if (duration) { 32 | setTimeout(() => { 33 | this.reloadFlag = true 34 | }, duration) 35 | } 36 | else { 37 | this.reloadFlag = true 38 | } 39 | setTimeout(() => { 40 | document.documentElement.scrollTo({ left: 0, top: 0 }) 41 | }, 100) 42 | }, 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /src/utils/cipher.ts: -------------------------------------------------------------------------------- 1 | import { decrypt, encrypt } from 'crypto-js/aes' 2 | import pkcs7 from 'crypto-js/pad-pkcs7' 3 | import ECB from 'crypto-js/mode-ecb' 4 | import md5 from 'crypto-js/md5' 5 | import UTF8, { parse } from 'crypto-js/enc-utf8' 6 | import Base64 from 'crypto-js/enc-base64' 7 | 8 | export interface EncryptionParams { 9 | key: string 10 | iv?: string 11 | } 12 | 13 | // 提供简单的aes-ECB加密,如有其他需求按需进行改造 14 | export class AesECBEncryption { 15 | private key 16 | private options 17 | 18 | constructor(options: EncryptionParams) { 19 | this.key = parse(options.key) 20 | this.options = { 21 | mode: ECB, 22 | padding: pkcs7, 23 | } 24 | } 25 | 26 | encryptByAES(cipherText: string) { 27 | if (this.key) 28 | return encrypt(cipherText, this.key, this.options).toString() 29 | } 30 | 31 | decryptByAES(cipherText: string) { 32 | if (this.key) 33 | return decrypt(cipherText, this.key, this.options).toString(UTF8) 34 | } 35 | } 36 | 37 | export function encryptByBase64(cipherText: string) { 38 | return Base64.parse(cipherText).toString(UTF8) 39 | } 40 | 41 | export function decodeByBase64(cipherText: string) { 42 | return Base64.parse(cipherText).toString(UTF8) 43 | } 44 | 45 | export function encryptByMd5(password: string) { 46 | return md5(password).toString() 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/router.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | /** 4 | * 判断根路由 Router 5 | */ 6 | export function isRootRouter(item: RouteRecordRaw) { 7 | return item.meta?.isRoot === true && item.children?.length === 1 8 | } 9 | 10 | /** 11 | * 排除Router 12 | */ 13 | export function filterRouter(routerMap: Array) { 14 | return routerMap.filter((item) => { 15 | return ( 16 | (item.meta?.hidden || false) !== true 17 | && !['/:path(.*)*', '/', '/redirect'].includes(item.path) 18 | ) 19 | }) 20 | } 21 | 22 | /** 23 | * 递归组装菜单格式 24 | */ 25 | export function generatorMenu(routerMap: Array) { 26 | return filterRouter(routerMap).map((item) => { 27 | const isRoot = isRootRouter(item) 28 | if (!item.children) { 29 | return { 30 | ...item, 31 | label: item.meta?.title, 32 | key: item.name, 33 | icon: item.meta?.icon, 34 | } 35 | } 36 | const info = isRoot ? item.children[0] : item 37 | const currentMenu = { 38 | ...info, 39 | ...info.meta, 40 | label: info.meta?.title, 41 | key: info.name, 42 | icon: isRoot ? item.meta?.icon : info.meta?.icon, 43 | } 44 | // 是否有子菜单,并递归处理 45 | if (!isRoot && info.children && info.children.length > 0) { 46 | // Recursion 47 | currentMenu.children = generatorMenu(info.children) 48 | } 49 | return currentMenu 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.cz-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'init', name: 'init: 项目初始化' }, 4 | { value: 'feat', name: 'feat: 添加新特性' }, 5 | { value: 'fix', name: 'fix: 修复bug' }, 6 | { value: 'docs', name: 'docs: 仅仅修改文档' }, 7 | { 8 | value: 'style', 9 | name: 'style: 仅仅修改了空格、格式缩进、逗号等等,不改变代码逻辑', 10 | }, 11 | { value: 'refactor', name: 'refactor: 代码重构,没有加新功能或者修复bug' }, 12 | { value: 'perf', name: 'perf: 优化相关,比如提升性能、体验' }, 13 | { value: 'test', name: 'test: 添加测试用例' }, 14 | { value: 'build', name: 'build: 依赖相关的内容' }, 15 | { 16 | value: 'ci', 17 | name: 'ci: CI配置相关,例如对k8s,docker的配置文件的修改', 18 | }, 19 | { value: 'chore', name: 'chore: 改变构建流程、或者增加依赖库、工具等' }, 20 | { value: 'revert', name: 'revert: 回滚到上一个版本' }, 21 | ], 22 | scopes: [ 23 | ['projects', '项目搭建'], 24 | ['components', '组件相关'], 25 | ['hooks', 'hook 相关'], 26 | ['utils', 'utils 相关'], 27 | ['types', 'ts类型相关'], 28 | ['styles', '样式相关'], 29 | ['deps', '项目依赖'], 30 | ['auth', '对 auth 修改'], 31 | ['other', '其他修改'], 32 | ['custom', '以上都不是?我要自定义'], 33 | ].map(([value, description]) => { 34 | return { 35 | value, 36 | name: `${value.padEnd(30)} (${description})`, 37 | } 38 | }), 39 | messages: { 40 | type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:', 41 | scope: '\n选择一个 scope(可选):', 42 | customScope: '请输入自定义的 scope:', 43 | subject: '填写简短精炼的变更描述:\n', 44 | body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n', 45 | breaking: '列举非兼容性重大的变更(可选):\n', 46 | footer: '列举出所有变更的 ISSUES CLOSED(可选)。 例如: #31, #34:\n', 47 | confirmCommit: '确认提交?', 48 | }, 49 | allowBreakingChanges: ['feat', 'fix'], 50 | subjectLimit: 100, 51 | breaklineChar: '|', 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/http/axiosCancel.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, Canceler } from 'axios' 2 | import axios from 'axios' 3 | import { isFunction } from '@/utils/is' 4 | 5 | // Used to store the identification and cancellation function of each request 6 | let pendingMap = new Map() 7 | 8 | export function getPendingUrl(config: AxiosRequestConfig) { 9 | return [config.method, config.url].join('&') 10 | } 11 | 12 | export class AxiosCanceler { 13 | /** 14 | * Add request 15 | * @param {object} config 16 | */ 17 | addPending(config: AxiosRequestConfig) { 18 | this.removePending(config) 19 | const url = getPendingUrl(config) 20 | config.cancelToken 21 | = config.cancelToken 22 | || new axios.CancelToken((cancel) => { 23 | if (!pendingMap.has(url)) { 24 | // If there is no current request in pending, add it 25 | pendingMap.set(url, cancel) 26 | } 27 | }) 28 | } 29 | 30 | /** 31 | * @description: Clear all pending 32 | */ 33 | removeAllPending() { 34 | pendingMap.forEach((cancel) => { 35 | cancel && isFunction(cancel) && cancel() 36 | }) 37 | pendingMap.clear() 38 | } 39 | 40 | /** 41 | * Removal request 42 | * @param {object} config 43 | */ 44 | removePending(config: AxiosRequestConfig) { 45 | const url = getPendingUrl(config) 46 | 47 | if (pendingMap.has(url)) { 48 | // If there is a current request identifier in pending, 49 | // the current request needs to be cancelled and removed 50 | const cancel = pendingMap.get(url) 51 | cancel && cancel(url) 52 | pendingMap.delete(url) 53 | } 54 | } 55 | 56 | /** 57 | * @description: reset 58 | */ 59 | reset(): void { 60 | pendingMap = new Map() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/views/example/HomePage.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /src/utils/http/index.ts: -------------------------------------------------------------------------------- 1 | // axios请求库 2 | import Axios from 'axios' 3 | // import { storage } from '@/utils/storage' 4 | 5 | // alova请求库 6 | import { createAlova } from 'alova' 7 | import GlobalFetch from 'alova/GlobalFetch' 8 | import VueHook from 'alova/vue' 9 | import { apiBaseUrl, requestTimeout } from '@/settings/index' 10 | 11 | // axios请求实例 12 | const axiosInstance = Axios.create({ 13 | baseURL: apiBaseUrl as string, 14 | timeout: requestTimeout, 15 | }) 16 | 17 | axiosInstance.interceptors.request.use( 18 | (config) => { 19 | // 按照业务需求填写,比如: 20 | 21 | // // 请求加上token 22 | // if (storage.get('token')) { 23 | // // jwt token 24 | // config.headers.Authorization = 'Bearer ' + storage.get('token') 25 | // } 26 | return config 27 | }, 28 | (err) => { 29 | // 错误处理 30 | console.error(err) 31 | return Promise.reject(err) 32 | }, 33 | ) 34 | 35 | axiosInstance.interceptors.response.use( 36 | (response) => { 37 | // 按照业务需求填写,比如只返回响应的body,不返回status code等信息 38 | return response.data 39 | }, 40 | (err) => { 41 | // 错误处理 42 | console.error(err) 43 | return Promise.reject(err) 44 | }, 45 | ) 46 | 47 | // alova请求实例 48 | const alovaInstance = createAlova({ 49 | // 假设我们需要与这个域名的服务器交互 50 | baseURL: apiBaseUrl, 51 | 52 | // 在vue项目下引入VueHook,它可以帮我们用vue的ref函数创建请求相关的,可以被alova管理的状态 53 | statesHook: VueHook, 54 | 55 | // 请求适配器,这里我们使用fetch请求适配器 56 | requestAdapter: GlobalFetch(), 57 | 58 | // 设置全局的请求拦截器,与axios相似 59 | // beforeRequest(config) { 60 | // // 假设我们需要添加token到请求头 61 | // if (storage.get('token')) { 62 | // config.config.headers.Authorization = 'Bearer ' + storage.get('token') 63 | // } 64 | // }, 65 | 66 | // 响应拦截器,也与axios类似 67 | async responsed(response) { 68 | const json = await response.json() 69 | if (json.code !== 200) { 70 | // 这边抛出错误时,将会进入请求失败拦截器内 71 | throw new Error(json.message) 72 | } 73 | return json.data 74 | }, 75 | }) 76 | 77 | export { axiosInstance as axios, alovaInstance as useRequest } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-starter-new", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "typecheck": "vue-tsc --noEmit --skipLibCheck", 8 | "dev": "vite", 9 | "build": "vue-tsc && vite build", 10 | "preview": "vite preview", 11 | "prepare": "npx simple-git-hooks", 12 | "cz": "cz", 13 | "release": "standard-version", 14 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", 15 | "lint": "eslint . --fix" 16 | }, 17 | "dependencies": { 18 | "@unocss/reset": "^0.59.4", 19 | "@vueuse/core": "^10.9.0", 20 | "alova": "^2.20.1", 21 | "axios": "^1.6.8", 22 | "dayjs": "^1.11.10", 23 | "lodash-es": "^4.17.21", 24 | "pinia": "^2.1.7", 25 | "vue": "^3.4.25", 26 | "vue-router": "^4.3.2" 27 | }, 28 | "devDependencies": { 29 | "@antfu/eslint-config": "^2.15.0", 30 | "@commitlint/cli": "^19.3.0", 31 | "@commitlint/config-conventional": "^19.2.2", 32 | "@iconify/json": "^2.2.204", 33 | "@iconify/vue": "^4.1.2", 34 | "@types/crypto-js": "^4.2.2", 35 | "@unocss/eslint-plugin": "^0.59.3", 36 | "@vitejs/plugin-vue": "^5.0.4", 37 | "@vitejs/plugin-vue-jsx": "^3.1.0", 38 | "commitizen": "^4.3.0", 39 | "cz-conventional-changelog": "^3.3.0", 40 | "cz-customizable": "^7.0.0", 41 | "eslint": "^9.1.1", 42 | "eslint-plugin-format": "^0.1.1", 43 | "lint-staged": "^15.2.2", 44 | "naive-ui": "^2.38.1", 45 | "prettier": "^3.2.5", 46 | "sass": "^1.75.0", 47 | "simple-git-hooks": "^2.11.1", 48 | "standard-version": "^9.5.0", 49 | "typescript": "^5.4.5", 50 | "unocss": "^0.59.4", 51 | "unplugin-auto-import": "^0.17.5", 52 | "unplugin-icons": "^0.18.5", 53 | "unplugin-vue-components": "^0.26.0", 54 | "vite": "^5.2.10", 55 | "vue-tsc": "^2.0.14" 56 | }, 57 | "simple-git-hooks": { 58 | "commit-msg": "npx --no -- commitlint --edit ${1}", 59 | "pre-commit": "pnpm typecheck && pnpm lint-staged" 60 | }, 61 | "lint-staged": { 62 | "*.{js,jsx,mjs,json,ts,tsx,vue}": "eslint . --fix" 63 | }, 64 | "config": { 65 | "commitizen": { 66 | "path": "./node_modules/cz-customizable" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/layouts/PcLayout.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /typings/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | HelloWorld: typeof import('./../src/components/HelloWorld.vue')['default'] 11 | 'IAntDesign:fullscreenExitOutlined': typeof import('~icons/ant-design/fullscreen-exit-outlined')['default'] 12 | 'IAntDesign:fullscreenOutlined': typeof import('~icons/ant-design/fullscreen-outlined')['default'] 13 | 'IIc:roundRefresh': typeof import('~icons/ic/round-refresh')['default'] 14 | 'ILineMd:githubLoop': typeof import('~icons/line-md/github-loop')['default'] 15 | 'ILineMd:moonAltToSunnyOutlineLoopTransition': typeof import('~icons/line-md/moon-alt-to-sunny-outline-loop-transition')['default'] 16 | 'ILineMd:sunnyFilledLoopToMoonAltFilledLoopTransition': typeof import('~icons/line-md/sunny-filled-loop-to-moon-alt-filled-loop-transition')['default'] 17 | 'ITabler:indentDecrease': typeof import('~icons/tabler/indent-decrease')['default'] 18 | 'ITabler:indentIncrease': typeof import('~icons/tabler/indent-increase')['default'] 19 | NaiveProvider: typeof import('./../src/components/common/NaiveProvider.vue')['default'] 20 | NaiveProviderContent: typeof import('./../src/components/common/NaiveProviderContent.vue')['default'] 21 | NButton: typeof import('naive-ui')['NButton'] 22 | NCard: typeof import('naive-ui')['NCard'] 23 | NGradientText: typeof import('naive-ui')['NGradientText'] 24 | NH1: typeof import('naive-ui')['NH1'] 25 | NH2: typeof import('naive-ui')['NH2'] 26 | NIcon: typeof import('naive-ui')['NIcon'] 27 | NLayout: typeof import('naive-ui')['NLayout'] 28 | NLayoutContent: typeof import('naive-ui')['NLayoutContent'] 29 | NLayoutFooter: typeof import('naive-ui')['NLayoutFooter'] 30 | NLayoutHeader: typeof import('naive-ui')['NLayoutHeader'] 31 | NLayoutSider: typeof import('naive-ui')['NLayoutSider'] 32 | NMenu: typeof import('naive-ui')['NMenu'] 33 | NTooltip: typeof import('naive-ui')['NTooltip'] 34 | RouterLink: typeof import('vue-router')['RouterLink'] 35 | RouterView: typeof import('vue-router')['RouterView'] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { URL, fileURLToPath } from 'node:url' 2 | import { defineConfig, loadEnv } from 'vite' 3 | import unocss from 'unocss/vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import autoImport from 'unplugin-auto-import/vite' 7 | import components from 'unplugin-vue-components/vite' 8 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 9 | import icons from 'unplugin-icons/vite' 10 | import IconsResolver from 'unplugin-icons/resolver' 11 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 12 | 13 | export default defineConfig((configEnv) => { 14 | const env = loadEnv(configEnv.mode, './') 15 | return { 16 | server: { 17 | proxy: { 18 | [env.VITE_API_URL]: { 19 | target: env.VITE_PROXY, 20 | changeOrigin: true, 21 | rewrite: (path: string) => 22 | path.replace(new RegExp(`^${env.VITE_API_URL}`), ''), 23 | }, 24 | }, 25 | }, 26 | resolve: { 27 | alias: { 28 | '@': fileURLToPath(new URL('./src', import.meta.url)), 29 | }, 30 | }, 31 | plugins: [ 32 | vue(), 33 | vueJsx(), 34 | unocss(), 35 | icons({ 36 | autoInstall: true, 37 | compiler: 'vue3', 38 | customCollections: { 39 | custom: FileSystemIconLoader( 40 | fileURLToPath(new URL('./src/assets/svg', import.meta.url)), 41 | ), 42 | }, 43 | }), 44 | autoImport({ 45 | dts: './typings/auto-imports.d.ts', 46 | imports: [ 47 | 'vue', 48 | 'vue-router', 49 | 'pinia', 50 | '@vueuse/core', 51 | { 52 | 'naive-ui': [ 53 | 'useDialog', 54 | 'useMessage', 55 | 'useNotification', 56 | 'useLoadingBar', 57 | ], 58 | }, 59 | ], 60 | dirs: ['./src/store/modules/**', './src/hooks/common/**'], 61 | eslintrc: { 62 | enabled: true, 63 | }, 64 | }), 65 | components({ 66 | dts: './typings/components.d.ts', 67 | types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }], 68 | resolvers: [ 69 | NaiveUiResolver(), 70 | IconsResolver({ 71 | customCollections: ['custom'], 72 | }), 73 | ], 74 | }), 75 | ], 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /src/router/guard.ts: -------------------------------------------------------------------------------- 1 | import { isNavigationFailure } from 'vue-router' 2 | import type { Router } from 'vue-router' 3 | import { useRouteStore } from '@/store/modules/route' 4 | import { storage } from '@/utils/storage' 5 | 6 | export function createGuard(router: Router) { 7 | router.beforeEach(async (_to, _from, next) => { 8 | const Loading = window.$loading || null 9 | Loading && Loading.start() 10 | 11 | // 可以执行token检查业务,比如token失效了跳转login页面 12 | const token = storage.get('token') 13 | if (!token) { 14 | // if (to.meta.ignoreAuth) { 15 | // next() 16 | // return 17 | // } 18 | // // redirect login page 19 | // const redirectData: { 20 | // path: string 21 | // replace: boolean 22 | // query?: Recordable 23 | // } = { 24 | // path: '/login', 25 | // replace: true, 26 | // } 27 | // if (to.path) { 28 | // redirectData.query = { 29 | // ...redirectData.query, 30 | // redirect: to.path, 31 | // } 32 | // } 33 | // next(redirectData) 34 | // return 35 | } 36 | 37 | next() 38 | Loading && Loading.finish() 39 | }) 40 | 41 | router.afterEach((to, _, failure) => { 42 | document.title = (to?.meta?.title as string) || document.title 43 | if (isNavigationFailure(failure)) 44 | console.error('failed navigation', failure) 45 | 46 | const routeStore = useRouteStore() 47 | // 在这里设置需要缓存的组件名称 48 | const keepAliveComponents = routeStore.keepAliveComponents 49 | const currentComName = to.matched.find(item => item.name === to.name) 50 | ?.name as string | undefined 51 | if ( 52 | currentComName 53 | && !keepAliveComponents.includes(currentComName) 54 | && to.meta?.keepAlive 55 | ) { 56 | // 需要缓存的组件 57 | keepAliveComponents.push(currentComName) 58 | } 59 | else if (!to.meta?.keepAlive || to.name === 'Redirect') { 60 | // 不需要缓存的组件 61 | const index = routeStore.keepAliveComponents.findIndex( 62 | name => name === currentComName, 63 | ) 64 | if (index !== -1) 65 | keepAliveComponents.splice(index, 1) 66 | } 67 | routeStore.setKeepAliveComponents(keepAliveComponents) 68 | 69 | const Loading = window.$loading || null 70 | Loading && Loading.finish() 71 | }) 72 | 73 | router.onError((error) => { 74 | console.error('路由错误:', error) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /src/router/modules/example.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import layout from '../../layouts/PcLayout.vue' 3 | import home from '~icons/tabler/home' 4 | import component from '~icons/icon-park-outline/figma-component' 5 | import info from '~icons/tabler/info-circle' 6 | import user from '~icons/tabler/user-circle' 7 | import blogger from '~icons/jam/blogger-circle' 8 | 9 | const routes: Array = [ 10 | { 11 | path: '/home', 12 | name: 'home', 13 | component: layout, 14 | meta: { 15 | isRoot: true, 16 | icon: () => h(home), 17 | }, 18 | children: [ 19 | { 20 | path: '', 21 | name: 'home-index', 22 | component: () => import('@/views/example/HomePage.vue'), 23 | meta: { 24 | title: '首页', 25 | }, 26 | }, 27 | ], 28 | }, 29 | { 30 | path: '/comp', 31 | name: 'comp', 32 | component: layout, 33 | meta: { 34 | title: '组件实例', 35 | icon: () => h(component), 36 | }, 37 | children: [ 38 | { 39 | path: 'keep-alive', 40 | name: 'KeepAlive', 41 | component: () => import('@/views/example/KeepAlive.vue'), 42 | meta: { 43 | title: '组件缓存', 44 | keepAlive: true, 45 | icon: () => h(component), 46 | }, 47 | }, 48 | { 49 | path: 'un-keep-alive', 50 | name: 'UnKeepAlive', 51 | component: () => import('@/views/example/UnKeep.vue'), 52 | meta: { 53 | title: '组件不缓存', 54 | icon: () => h(component), 55 | }, 56 | }, 57 | ], 58 | }, 59 | { 60 | path: '/about', 61 | name: 'about', 62 | component: layout, 63 | meta: { 64 | icon: () => h(info), 65 | title: '关于', 66 | }, 67 | children: [ 68 | { 69 | path: 'project', 70 | name: 'https://github.com/MatrixCross/Vue3-PCWeb-Starter', 71 | component: layout, 72 | meta: { 73 | title: '关于项目', 74 | icon: () => h(info), 75 | }, 76 | }, 77 | { 78 | path: 'author', 79 | name: 'https://github.com/Wyatex', 80 | component: layout, 81 | meta: { 82 | title: '关于作者', 83 | icon: () => h(user), 84 | }, 85 | }, 86 | ], 87 | }, 88 | { 89 | path: '/blog', 90 | name: 'https://wyatex.work', 91 | component: layout, 92 | meta: { 93 | title: '作者博客', 94 | icon: () => h(blogger), 95 | isRoot: true, 96 | }, 97 | }, 98 | ] 99 | 100 | export default routes 101 | -------------------------------------------------------------------------------- /src/assets/styles/transition/fade.scss: -------------------------------------------------------------------------------- 1 | // /////////////////////////////////////////////// 2 | // Fade Base 3 | // /////////////////////////////////////////////// 4 | .fade { 5 | &-enter-active, 6 | &-leave-active { 7 | transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1) !important; 8 | } 9 | 10 | &-enter-from, 11 | &-leave-to { 12 | opacity: 0; 13 | } 14 | } 15 | 16 | // /////////////////////////////////////////////// 17 | // Fade Left 18 | // /////////////////////////////////////////////// 19 | 20 | .fade-left-leave-active, 21 | .fade-left-enter-active { 22 | transition: all 0.3s; 23 | } 24 | 25 | .fade-left-enter-from { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | .fade-left-leave-to { 31 | opacity: 0; 32 | transform: translateX(-30px); 33 | } 34 | 35 | // /////////////////////////////////////////////// 36 | // Fade Right 37 | // /////////////////////////////////////////////// 38 | 39 | .fade-right-leave-active, 40 | .fade-right-enter-active { 41 | transition: all 0.3s; 42 | } 43 | 44 | .fade-right-enter-from { 45 | opacity: 0; 46 | transform: translateX(-30px); 47 | } 48 | 49 | .fade-right-leave-to { 50 | opacity: 0; 51 | transform: translateX(30px); 52 | } 53 | 54 | // /////////////////////////////////////////////// 55 | // Fade Top 56 | // /////////////////////////////////////////////// 57 | 58 | .fade-top-enter-active, 59 | .fade-top-leave-active { 60 | transition: opacity 0.25s, transform 0.3s; 61 | } 62 | 63 | .fade-top-enter-from { 64 | opacity: 0; 65 | transform: translateY(10%); 66 | } 67 | 68 | .fade-top-leave-to { 69 | opacity: 0; 70 | transform: translateY(-10%); 71 | } 72 | 73 | // /////////////////////////////////////////////// 74 | // Fade Bottom 75 | // /////////////////////////////////////////////// 76 | 77 | .fade-bottom-enter-active, 78 | .fade-bottom-leave-active { 79 | transition: opacity 0.25s, transform 0.3s; 80 | } 81 | 82 | .fade-bottom-enter-from { 83 | opacity: 0; 84 | transform: translateY(-10%); 85 | } 86 | 87 | .fade-bottom-leave-to { 88 | opacity: 0; 89 | transform: translateY(10%); 90 | } 91 | 92 | // /////////////////////////////////////////////// 93 | // Fade In 94 | // /////////////////////////////////////////////// 95 | 96 | .fade-in-leave-active, 97 | .fade-in-enter-active { 98 | transition: all 0.28s; 99 | } 100 | 101 | .fade-in-enter-from { 102 | opacity: 0; 103 | transform: scale(0.8); 104 | } 105 | 106 | .fade-in-leave-to { 107 | opacity: 0; 108 | transform: scale(1.2); 109 | } 110 | 111 | // /////////////////////////////////////////////// 112 | // Fade Out 113 | // /////////////////////////////////////////////// 114 | 115 | .fade-out-leave-active, 116 | .fade-out-enter-active { 117 | transition: all 0.28s; 118 | } 119 | 120 | .fade-out-enter-from { 121 | opacity: 0; 122 | transform: scale(1.2); 123 | } 124 | 125 | .fade-out-leave-to { 126 | opacity: 0; 127 | transform: scale(0.8); 128 | } 129 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString 2 | 3 | /** 4 | * @description: 判断值是否未某个类型 5 | */ 6 | export function is(val: unknown, type: string) { 7 | return toString.call(val) === `[object ${type}]` 8 | } 9 | 10 | /** 11 | * @description: 是否为函数 12 | */ 13 | export function isFunction(val: unknown): val is T { 14 | return is(val, 'Function') 15 | } 16 | 17 | /** 18 | * @description: 是否已定义 19 | */ 20 | export function isDef(val?: T): val is T { 21 | return typeof val !== 'undefined' 22 | } 23 | 24 | export function isUnDef(val?: T): val is T { 25 | return !isDef(val) 26 | } 27 | /** 28 | * @description: 是否为对象 29 | */ 30 | export function isObject>(val: unknown): val is T { 31 | return is(val, 'Object') 32 | } 33 | 34 | /** 35 | * @description: 是否为时间 36 | */ 37 | export function isDate(val: unknown): val is Date { 38 | return is(val, 'Date') 39 | } 40 | 41 | /** 42 | * @description: 是否为数值 43 | */ 44 | export function isNumber(val: unknown): val is number { 45 | return is(val, 'Number') 46 | } 47 | 48 | /** 49 | * @description: 是否为AsyncFunction 50 | */ 51 | export function isAsyncFunction( 52 | val: unknown, 53 | ): val is (...args: U[]) => Promise { 54 | return is(val, 'AsyncFunction') 55 | } 56 | 57 | /** 58 | * @description: 是否为promise 59 | */ 60 | export function isPromise(val: unknown): val is Promise { 61 | return ( 62 | is(val, 'Promise') 63 | && isObject(val) 64 | && isFunction(val.then) 65 | && isFunction(val.catch) 66 | ) 67 | } 68 | 69 | /** 70 | * @description: 是否为字符串 71 | */ 72 | export function isString(val: unknown): val is string { 73 | return is(val, 'String') 74 | } 75 | 76 | /** 77 | * @description: 是否为boolean类型 78 | */ 79 | export function isBoolean(val: unknown): val is boolean { 80 | return is(val, 'Boolean') 81 | } 82 | 83 | /** 84 | * @description: 是否为数组 85 | */ 86 | export function isArray(val: unknown): val is Array { 87 | return Array.isArray(val) 88 | } 89 | 90 | export function isNull(val: unknown): val is null { 91 | return val === null 92 | } 93 | 94 | export function isNullOrUnDef(val: unknown): val is null | undefined { 95 | return isUnDef(val) || isNull(val) 96 | } 97 | 98 | /** 99 | * @description: 是否客户端 100 | */ 101 | export function isClient() { 102 | return typeof window !== 'undefined' 103 | } 104 | 105 | /** 106 | * @description: 是否为浏览器 107 | */ 108 | export function isWindow(val: unknown): val is Window { 109 | return typeof window !== 'undefined' && is(val, 'Window') 110 | } 111 | 112 | export function isElement(val: unknown): val is Element { 113 | return isObject(val) && !!val.tagName 114 | } 115 | 116 | export const isServer = typeof window === 'undefined' 117 | 118 | // 是否为图片节点 119 | export function isImageDom(o: Element) { 120 | return o && ['IMAGE', 'IMG'].includes(o.tagName) 121 | } 122 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetIcons, presetUno } from 'unocss' 2 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 3 | 4 | export default defineConfig({ 5 | content: { 6 | pipeline: { 7 | exclude: ['node_modules', '.git', 'dist', 'mock', './stats.html'], 8 | }, 9 | }, 10 | presets: [ 11 | presetUno({ dark: 'class' }), 12 | presetIcons({ 13 | collections: { 14 | custom: FileSystemIconLoader('./src/assets/svg', svg => 15 | svg.replace(/#fff/, 'currentColor')), 16 | }, 17 | }), 18 | ], 19 | shortcuts: { 20 | 'wh-full': 'w-full h-full', 21 | 'flex-center': 'flex justify-center items-center', 22 | 'flex-col-center': 'flex-center flex-col', 23 | 'flex-x-center': 'flex justify-center', 24 | 'flex-y-center': 'flex items-center', 25 | 'i-flex-center': 'inline-flex justify-center items-center', 26 | 'i-flex-x-center': 'inline-flex justify-center', 27 | 'i-flex-y-center': 'inline-flex items-center', 28 | 'flex-col': 'flex flex-col', 29 | 'flex-col-stretch': 'flex-col items-stretch', 30 | 'i-flex-col': 'inline-flex flex-col', 31 | 'i-flex-col-stretch': 'i-flex-col items-stretch', 32 | 'flex-1-hidden': 'flex-1 overflow-hidden', 33 | 'absolute-lt': 'absolute left-0 top-0', 34 | 'absolute-lb': 'absolute left-0 bottom-0', 35 | 'absolute-rt': 'absolute right-0 top-0', 36 | 'absolute-rb': 'absolute right-0 bottom-0', 37 | 'absolute-tl': 'absolute-lt', 38 | 'absolute-tr': 'absolute-rt', 39 | 'absolute-bl': 'absolute-lb', 40 | 'absolute-br': 'absolute-rb', 41 | 'absolute-center': 'absolute-lt flex-center wh-full', 42 | 'fixed-lt': 'fixed left-0 top-0', 43 | 'fixed-lb': 'fixed left-0 bottom-0', 44 | 'fixed-rt': 'fixed right-0 top-0', 45 | 'fixed-rb': 'fixed right-0 bottom-0', 46 | 'fixed-tl': 'fixed-lt', 47 | 'fixed-tr': 'fixed-rt', 48 | 'fixed-bl': 'fixed-lb', 49 | 'fixed-br': 'fixed-rb', 50 | 'fixed-center': 'fixed-lt flex-center wh-full', 51 | 'nowrap-hidden': 'whitespace-nowrap overflow-hidden', 52 | 'ellipsis-text': 'nowrap-hidden overflow-ellipsis', 53 | 'transition-base': 'transition-all duration-300 ease-in-out', 54 | }, 55 | theme: { 56 | colors: { 57 | primary: 'var(--primary-color)', 58 | primary_hover: 'var(--primary-color-hover)', 59 | primary_pressed: 'var(--primary-color-pressed)', 60 | primary_active: 'var(--primary-color-active)', 61 | info: 'var(--info-color)', 62 | info_hover: 'var(--info-color-hover)', 63 | info_pressed: 'var(--info-color-pressed)', 64 | info_active: 'var(--info-color-active)', 65 | success: 'var(--success-color)', 66 | success_hover: 'var(--success-color-hover)', 67 | success_pressed: 'var(--success-color-pressed)', 68 | success_active: 'var(--success-color-active)', 69 | warning: 'var(--warning-color)', 70 | warning_hover: 'var(--warning-color-hover)', 71 | warning_pressed: 'var(--warning-color-pressed)', 72 | warning_active: 'var(--warning-color-active)', 73 | error: 'var(--error-color)', 74 | error_hover: 'var(--error-color-hover)', 75 | error_pressed: 'var(--error-color-pressed)', 76 | error_active: 'var(--error-color-active)', 77 | dark: '#18181c', 78 | }, 79 | }, 80 | }) 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 Starter 2 | 3 | [![Author](https://img.shields.io/badge/Author-Wyatex-green)](https://github.com/Wyatex/) 4 | [![Issues](https://img.shields.io/github/issues/Wyatex/Vue3-starter)](https://github.com/Wyatex/Vue3-starter/issues) 5 | [![License](https://img.shields.io/badge/License-MIT-yellowgreen)](https://github.com/Wyatex/Vue3-starter/blob/master/LICENSE) 6 | 7 | > 一套集成了 Vite 3.x + Vue 3.x + TypeScript + Vue Router + Pinia + Axios + ESlint 等套件的快速开发模板。 8 | 9 | [在线预览](https://vue3-pc-web-starter.wyatex.online/) 10 | 11 | ## 快速开始 12 | 13 | ### 使用cli创建项目 14 | 15 | ```sh 16 | pnpm create matrix-starter 17 | ``` 18 | 19 | 选择 vue3 - PCWeb-NaiveUI 20 | 21 | ### 安装依赖 22 | 23 | ```sh 24 | pnpm i 25 | ``` 26 | 27 | ### 启动 dev 服务器和构建 28 | 29 | 启动 dev 服务器 30 | 31 | ```sh 32 | pnpm dev 33 | ``` 34 | 35 | 构建,默认打包到'./dist' 36 | 37 | ```sh 38 | pnpm build 39 | ``` 40 | 41 | ## 技术栈 42 | 43 | - 编程语言:[Typescript](https://www.typescriptlang.org/zh/) + [Javascript](https://www.javascript.com/) 44 | - 构建工具:[Vite4](https://vitejs.cn/) 45 | - 前端框架:[Vue3](https://v3.cn.vuejs.org/) 46 | - 路由管理:[Vue-Router4](https://next.router.vuejs.org/zh/index.html) 47 | - 状态管理:[pinia](https://pinia.esm.dev/) 48 | - CSS 预处理:[Sass](https://sass-lang.com/) 49 | - CSS 引擎:[UnoCSS](https://github.com/unocss/unocss) 50 | - HTTP 工具:[Axios](https://axios-http.com/) + [Alova](https://alova.js.org/zh-CN/) 51 | - JSX 处理:[@vitejs/plugin-vue-jsx](https://www.npmjs.com/package/@vitejs/plugin-vue-jsx) 52 | - 代码规范:[ESLint](https://eslint.org/) 53 | - 代码格式化:[Prettier](https://prettier.io/) 54 | - 组件库:[Naive-UI](https://www.naiveui.com/) 55 | - Icon 库:[@iconify/json](https://www.npmjs.com/package/@iconify/json) + [@iconify/vue](https://www.npmjs.com/package/@iconify/vue) + [unplugin-icons](https://github.com/antfu/unplugin-icons) + [unocss-icon](https://unocss.dev/presets/icons) 56 | 57 | > icon 图标查找:[icones](https://icones.js.org/) 58 | 59 | ## 项目配置 60 | 61 | 目前的主要配置项: 62 | 63 | - src/settings/index.ts 64 | - .env(.development / .production) 65 | - vite.config.ts 66 | 67 | ## 图标使用 68 | 69 | 如果是静态的 icon,可以使用 unplugin-icons 或者 unocss 70 | 71 | ```html 72 | 73 | 74 | 75 |
76 |