diff --git a/.env b/.env index a8db8f6..6b20d11 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # 应用名称 -VITE_APP_TITLE=DashboardTemplate +VITE_APP_TITLE="Vite 模板" # 平台本地运行端口号 VITE_PORT=7000 diff --git a/.env.development b/.env.development index 51754c2..d8b465e 100644 --- a/.env.development +++ b/.env.development @@ -11,4 +11,7 @@ VITE_APP_URL=http://localhost:8801 VITE_STRICT_PORT=false # 是否在打包时使用cdn替换本地库 替换 true 不替换 false -VITE_CDN=gzip \ No newline at end of file +VITE_CDN=false + +# 是否启用gzip压缩 +VITE_COMPRESSION=gzip \ No newline at end of file diff --git a/.env.production b/.env.production index 523087d..0ed6804 100644 --- a/.env.production +++ b/.env.production @@ -1,5 +1,5 @@ # 平台本地运行端口号 -VITE_PORT=8888 +VITE_PORT=8800 # 开发环境读取配置文件路径 VITE_PUBLIC_PATH=/ @@ -11,4 +11,7 @@ VITE_APP_URL=http://localhost:8000 VITE_STRICT_PORT=false # 是否在打包时使用cdn替换本地库 替换 true 不替换 false -VITE_CDN=gzip \ No newline at end of file +VITE_CDN=false + +# 是否启用gzip压缩 +VITE_COMPRESSION=gzip \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f3e9850 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +dist +node_modules +public +.husky +.vscode +.idea +*.sh +*.md + +src/assets +stats.html diff --git a/.prettierrc.js b/.prettierrc.js index b110280..154480d 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,38 +1,46 @@ -// @see: https://www.prettier.cn +module.exports = { + // (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always) + arrowParens: "always", + // 开始标签的右尖括号是否跟随在最后一行属性末尾,默认false + bracketSameLine: false, + // 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar}) + bracketSpacing: true, + // 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto) + embeddedLanguageFormatting: "auto", + // 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css) + htmlWhitespaceSensitivity: "ignore", + // 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记,默认false + insertPragma: false, + // 在 JSX 中使用单引号替代双引号,默认false + jsxSingleQuote: false, + // 每行最多字符数量,超出换行(默认100) + printWidth: 100, + // 超出打印宽度 (always | never | preserve ) + proseWrap: "preserve", + // 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;) + quoteProps: "as-needed", + // 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件,默认false + requirePragma: false, + // 结尾添加分号 + semi: true, + // 使用单引号 (true:单引号;false:双引号) + singleQuote: false, + // 缩进空格数,默认2个空格 + tabWidth: 2, + // 元素末尾是否加逗号,默认es5: ES5中的 objects, arrays 等会添加逗号,TypeScript 中的 type 后不加逗号 + trailingComma: "es5", + // 指定缩进方式,空格或tab,默认false,即使用空格 + useTabs: false, + // vue 文件中是否缩进 diff --git a/src/main.ts b/src/main.ts index f8c23e4..062319e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,9 @@ import { createApp } from 'vue'; -import './style.css'; +import 'animate.css'; import App from './App.vue'; +import plugins from '@/plugins'; +import '@unocss/reset/tailwind-compat.css'; +import 'uno.css'; +import 'virtual:unocss-devtools'; -createApp(App).mount('#app'); +createApp(App).use(plugins).mount('#app'); diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..c11597a --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,15 @@ +import type { App } from 'vue'; +import { setupDirective } from '@/directive'; +import { setUpRouter } from '@/router'; +import { setupStore } from '@/store'; + +export default { + install(app: App) { + // 设置路由 + setUpRouter(app); + // 设置状态管理 + setupStore(app); + // 设置指令 + setupDirective(app); + }, +}; diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..ac3dcef --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,61 @@ +import type { App } from 'vue'; +import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'; + +export const Layout = () => import('@/layout/index.vue'); + +// 静态路由 +const routes: RouteRecordRaw[] = [ + { + path: '/redirect', + component: Layout, + meta: { hidden: true }, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect/index.vue'), + }, + ], + }, + { + path: '/', + name: '/', + component: Layout, + // redirect: '/dashboard', + children: [ + // { + // path: 'dashboard', + // component: () => import('@/views/index.vue'), + // // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致 + // // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude + // name: 'Dashboard', + // meta: { + // title: 'dashboard', + // icon: 'homepage', + // affix: true, + // keepAlive: true, + // }, + // }, + ], + }, + { + path: '/404', + component: () => import('@/views/error-page/404.vue'), + meta: { hidden: true }, + }, +]; +const router = createRouter({ + history: createWebHashHistory(), + routes, + scrollBehavior: () => ({ x: 0, y: 0 }), +}); + +/** 全局注册 router */ +export const setUpRouter = (app: App) => { + app.use(router); +}; + +/** 重置路由 */ +export const resetRouter = () => { + router.replace({ path: '/' }).then(); +}; +export default router; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..24ed9e4 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,9 @@ +import type { App } from 'vue'; +import { createPinia } from 'pinia'; + +const store = createPinia(); + +// 全局注册 store +export function setupStore(app: App) { + app.use(store); +} diff --git a/src/style.css b/src/style.css deleted file mode 100644 index a15dbab..0000000 --- a/src/style.css +++ /dev/null @@ -1,84 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} - -button:hover { - border-color: #646cff; -} - -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index ec15153..a8afb09 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,6 +1,6 @@ declare global { /* 环境便配置 */ - interface ViteEnv { + declare interface ViteEnv { VITE_APP_TITLE: string; VITE_PORT: number; VITE_PUBLIC_PATH: string; diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..0b3aa70 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1 @@ +type Recordable = Record; diff --git a/src/utils/nprogress.ts b/src/utils/nprogress.ts new file mode 100644 index 0000000..e9ba431 --- /dev/null +++ b/src/utils/nprogress.ts @@ -0,0 +1,18 @@ +import NProgress from 'nprogress'; +import 'nprogress/nprogress.css'; + +// 进度条 +NProgress.configure({ + // 动画方式 + easing: 'ease', + // 递增进度条的速度 + speed: 500, + // 是否显示加载ico + showSpinner: false, + // 自动递增间隔 + trickleSpeed: 200, + // 初始化时的最小百分比 + minimum: 0.3, +}); + +export default NProgress; diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..7944688 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,71 @@ +import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'; +import { useUserStoreHook } from '@/store/modules/user'; +import { ResultEnum } from '@/enums/ResultEnum'; +import { TOKEN_KEY } from '@/enums/CacheEnum'; +import qs from 'qs'; + +// 创建 axios 实例 +const service = axios.create({ + baseURL: import.meta.env.VITE_APP_BASE_API, + timeout: 50000, + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + paramsSerializer: params => { + return qs.stringify(params); + }, +}); + +// 请求拦截器 +service.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const accessToken = localStorage.getItem(TOKEN_KEY); + if (accessToken) { + config.headers.Authorization = accessToken; + } + return config; + }, + (error: any) => { + return Promise.reject(error); + }, +); + +// 响应拦截器 +service.interceptors.response.use( + (response: AxiosResponse) => { + // 检查配置的响应类型是否为二进制类型('blob' 或 'arraybuffer'), 如果是,直接返回响应对象 + if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') { + return response; + } + + const { code, data, msg } = response.data; + if (code === ResultEnum.SUCCESS) { + return data; + } + + ElMessage.error(msg || '系统出错'); + return Promise.reject(new Error(msg || 'Error')); + }, + (error: any) => { + // 异常处理 + if (error.response.data) { + const { code, msg } = error.response.data; + if (code === ResultEnum.TOKEN_INVALID) { + ElNotification({ + title: '提示', + message: '您的会话已过期,请重新登录', + type: 'info', + }); + useUserStoreHook() + .resetToken() + .then(() => { + location.reload(); + }); + } else { + ElMessage.error(msg || '系统出错'); + } + } + return Promise.reject(error.message); + }, +); + +// 导出 axios 实例 +export default service; diff --git a/src/views/error-page/404.vue b/src/views/error-page/404.vue new file mode 100644 index 0000000..f8e1007 --- /dev/null +++ b/src/views/error-page/404.vue @@ -0,0 +1,232 @@ + + + + + + + + + + + + OOPS! + + All rights reserved + wallstreetcn + + The webmaster said that you can not enter this page... + Please check that the URL you entered is correct, or click the button below to return to the homepage. + Back to home + + + + + diff --git a/src/views/index.vue b/src/views/index.vue new file mode 100644 index 0000000..77a8143 --- /dev/null +++ b/src/views/index.vue @@ -0,0 +1,5 @@ + + +初始化页面 + + diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue new file mode 100644 index 0000000..56f37a6 --- /dev/null +++ b/src/views/redirect/index.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 0000000..2e16401 --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,51 @@ +module.exports = { + // 继承推荐规范配置 + extends: [ + 'stylelint-config-standard', + 'stylelint-config-recommended-scss', + 'stylelint-config-recommended-vue/scss', + 'stylelint-config-html/vue', + 'stylelint-config-recess-order', + ], + // 指定不同文件对应的解析器 + overrides: [ + { + files: ['**/*.{vue,html}'], + customSyntax: 'postcss-html', + }, + { + files: ['**/*.{css,scss}'], + customSyntax: 'postcss-scss', + }, + ], + // 自定义规则 + rules: { + 'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url") + 'selector-class-pattern': null, // 选择器类名命名规则 + 'custom-property-pattern': null, // 自定义属性命名规则 + 'keyframes-name-pattern': null, // 动画帧节点样式命名规则 + 'no-descending-specificity': null, // 允许无降序特异性 + 'no-empty-source': null, // 允许空样式 + // 允许 global 、export 、deep伪类 + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global', 'export', 'deep'], + }, + ], + // 允许未知属性 + 'property-no-unknown': [ + true, + { + ignoreProperties: [], + }, + ], + // 允许未知规则 + 'at-rule-no-unknown': [ + true, + { + ignoreAtRules: ['apply', 'use', 'forward'], + }, + ], + }, +}; diff --git a/tsconfig.app.json b/tsconfig.app.json deleted file mode 100644 index 7fb078c..0000000 --- a/tsconfig.app.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "@vue/tsconfig/tsconfig.dom.json", - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] -} diff --git a/tsconfig.json b/tsconfig.json index fa025e9..5633b67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,38 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "noLib": false, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": [ + "esnext", + "dom" + ], + "baseUrl": ".", + "allowJs": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "jsx": "preserve", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment", + "paths": { + "@/*": [ + "src/*" + ] }, - { - "path": "./tsconfig.node.json" - } - ], + "types": [ + "vite/client", + "unplugin-icons/types/vue", + "element-plus/global" + ] + }, + "files": [], "include": [ "mock/*.ts", "src/**/*.ts", diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index db0becc..0000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..5e8798b --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,33 @@ +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + presetWebFonts, + transformerDirectives, + transformerVariantGroup, +} from 'unocss'; + +export default defineConfig({ + shortcuts: [ + // ... + ], + theme: { + colors: { + // ... + }, + }, + presets: [ + presetUno(), + presetAttributify(), + presetIcons(), + presetTypography(), + presetWebFonts({ + fonts: { + // ... + }, + }), + ], + transformers: [transformerDirectives(), transformerVariantGroup()], +});