├── .env ├── .env.development ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.tsx ├── api │ ├── types │ │ └── user.ts │ └── user.ts ├── assets │ ├── logo.png │ ├── screenshot │ │ └── screen01.png │ └── user.png ├── components │ ├── ABreadCrumb │ │ ├── index.module.less │ │ └── index.tsx │ ├── Exception │ │ ├── index.module.less │ │ ├── index.tsx │ │ └── type.ts │ ├── GlobalBg │ │ ├── index.module.less │ │ └── index.tsx │ └── GlobalHeader │ │ ├── GlobalHeader.tsx │ │ ├── LevelMenus.tsx │ │ ├── Menus.tsx │ │ ├── index.module.less │ │ └── index.tsx ├── layouts │ ├── BlankLayout.tsx │ ├── LevelBasicLayout.tsx │ ├── RouteLayout.tsx │ ├── SimplifyBasicLayout.tsx │ ├── index.module.less │ └── index.ts ├── main.ts ├── mock │ ├── index.ts │ ├── result.ts │ └── user │ │ ├── login.ts │ │ ├── logout.ts │ │ └── permission.ts ├── plugins │ └── antd.ts ├── public │ ├── css │ │ ├── base.css │ │ └── init.less │ └── font │ │ ├── demo.css │ │ ├── demo_index.html │ │ ├── iconfont.css │ │ ├── iconfont.js │ │ ├── iconfont.json │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 ├── router │ ├── index.ts │ └── router.config.ts ├── store │ ├── index.ts │ └── modules │ │ └── common.ts ├── types │ ├── interfaces.d.ts │ ├── shims-vue.d.ts │ └── vite-env.d.ts ├── utils │ ├── const.ts │ ├── fetch.ts │ ├── resetMessage.js │ ├── socket.ts │ ├── token.ts │ ├── util.ts │ └── validate.ts └── views │ ├── dataManage │ ├── index.module.less │ └── index.tsx │ ├── dataProtal │ ├── index.module.less │ └── index.tsx │ ├── designCenter │ ├── materialMange │ │ ├── customControl │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ └── customMaterial │ │ │ ├── index.module.less │ │ │ └── index.tsx │ └── screenManage │ │ ├── index.module.less │ │ └── index.tsx │ ├── exception │ ├── 403.tsx │ ├── 404.tsx │ └── 500.tsx │ ├── home │ ├── index.module.less │ └── index.tsx │ ├── user │ └── login │ │ ├── index.module.less │ │ └── index.tsx │ └── userManage │ ├── index.module.less │ └── index.tsx ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=/api -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=/api -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | jsx: true, 9 | }, 10 | }, 11 | extends: [ 12 | 'plugin:vue/vue3-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'prettier/@typescript-eslint', 15 | 'plugin:prettier/recommended', 16 | ], 17 | rules: { 18 | '@typescript-eslint/ban-ts-ignore': 'off', 19 | '@typescript-eslint/explicit-function-return-type': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | '@typescript-eslint/no-var-requires': 'off', 22 | '@typescript-eslint/no-empty-function': 'off', 23 | 'vue/custom-event-name-casing': 'off', 24 | 'no-use-before-define': 'off', 25 | '@typescript-eslint/no-use-before-define': 'off', 26 | '@typescript-eslint/ban-ts-comment': 'off', 27 | '@typescript-eslint/ban-types': 'off', 28 | '@typescript-eslint/no-non-null-assertion': 'off', 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | '@typescript-eslint/no-unused-vars': [ 31 | 'error', 32 | { 33 | argsIgnorePattern: '^h$', 34 | varsIgnorePattern: '^h$', 35 | }, 36 | ], 37 | 'no-unused-vars': [ 38 | 'error', 39 | { 40 | argsIgnorePattern: '^h$', 41 | varsIgnorePattern: '^h$', 42 | }, 43 | ], 44 | 'space-before-function-paren': 'off', 45 | quotes: ['error', 'single'], 46 | 'comma-dangle': ['error', 'never'], 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 175, 3 | "trailingComma": "none", 4 | "tabWidth": 2, 5 | "useTabs": true, 6 | "semi": false, 7 | "singleQuote": true, 8 | "arrowParens": "avoid", 9 | "bracketSpacing": true, 10 | "proseWrap": "preserve", 11 | "htmlWhitespaceSensitivity": "ignore", 12 | "jsxBracketSameLine": true 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 随着`Vue3`的普及,已经有越来越多的项目开始使用Vue3。为了快速进入开发状态,在这里向大家推荐一套`开箱即用`的企业级开发脚手架,框架使用:`Vue3` + `Vite2` + `TypeScript` + `JSX` + `Pinia(Vuex)` + `Antd`。废话不多话,直接上手开撸。 2 | **该脚手架根据使用状态库的不同分为两个版本Vuex版、Pinia版,下面是相关代码地址:** 3 | [Vuex版](https://github.com/sunshine824/vue3.0-typescript-starter/tree/master)、 4 | [Pinia版](https://github.com/sunshine824/vue3.0-typescript-starter/tree/pinia) 5 | 6 | ## 搭建需准备 7 | 8 | 1. [Vscode](https://code.visualstudio.com/Download) : 前端人必备写码神器 9 | 10 | 2. [Chrome](https://www.google.cn/chrome/index.html) :对开发者非常友好的浏览器(程序员标配浏览器) 11 | 12 | 3. [Nodejs & npm](https://nodejs.org/zh-cn/download/) :配置本地开发环境,安装 Node 后你会发现 npm 也会一起安装下来 (V12+) 13 | 14 | > 使用npm安装依赖包时会发现非常慢,在这里推荐使用cnpm、yarn代替。 15 | 16 | ## 脚手架目录结构 17 | ``` 18 | ├── src 19 | │   ├── App.tsx 20 | │ ├── api # 接口管理模块 21 | │ ├── assets # 静态资源模块 22 | │ ├── components # 公共组件模块 23 | │ ├── i18n # 国际化模块 24 | │ ├── mock # mock接口模拟模块 25 | │ ├── layouts # 公共自定义布局 26 | │ ├── main.ts # 入口文件 27 | │ ├── public # 公共资源模块 28 | │ ├── router # 路由 29 | │ ├── store # vuex状态库 30 | │ ├── types # 声明文件 31 | │ ├── utils # 公共方法模块 32 | │ └── views # 视图模块 33 | ├── tsconfig.json 34 | └── vite.config.js 35 | ``` 36 | 37 | ## 什么是Vite 38 | 39 | > 下一代前端开发与构建工具 40 | Vite(法语意为 "快速的",发音 `/vit/`,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成: 41 | 42 | - 一个开发服务器,它基于 [原生 ES 模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 提供了 [丰富的内建功能](https://vitejs.cn/guide/features.html),如速度快到惊人的 [模块热更新(HMR)](https://vitejs.cn/guide/features.html#hot-module-replacement)。 43 | - 一套构建指令,它使用 [Rollup](https://rollupjs.org/) 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。 44 | 45 | Vite 意在提供开箱即用的配置,同时它的 [插件 API](https://vitejs.cn/guide/api-plugin.html) 和 [JavaScript API](https://vitejs.cn/guide/api-javascript.html) 带来了高度的可扩展性,并有完整的类型支持。 46 | 47 | 你可以在 [为什么选 Vite](https://vitejs.cn/guide/why.html) 中了解更多关于项目的设计初衷。 48 | 49 | ## 什么是Pinia 50 | Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇 51 | 52 | Pinia.js 有如下特点: 53 | - 相比Vuex更加完整的 typescript 的支持; 54 | - 足够轻量,压缩后的体积只有1.6kb; 55 | - 去除 mutations,只有 state,getters,actions(支持同步和异步); 56 | - 使用相比Vuex更加方便,每个模块独立,更好的代码分割,没有模块嵌套,store之间可以自由使用 57 | 58 | ### 安装 59 | 60 | ```js 61 | npm install pinia --save 62 | ``` 63 | ### 创建Store 64 | - 新建 src/store 目录并在其下面创建 index.ts,并导出store 65 | ```js 66 | import { createPinia } from 'pinia' 67 | 68 | const store = createPinia() 69 | 70 | export default store 71 | ``` 72 | - 在main.ts中引入 73 | 74 | ```js 75 | import { createApp } from 'vue' 76 | import store from './store' 77 | 78 | const app = createApp(App) 79 | 80 | app.use(store) 81 | ``` 82 | ### 定义State 83 | 在新建src/store/modules,根据模块划分在modules下新增common.ts 84 | 85 | 86 | ```js 87 | import { defineStore } from 'pinia' 88 | 89 | export const CommonStore = defineStore('common', { 90 | // 状态库 91 | state: () => ({ 92 | userInfo: null, //用户信息 93 | }), 94 | }) 95 | ``` 96 | ### 获取State 97 | 获取state有多种方式,最常用一下几种: 98 | 99 | ```js 100 | import { CommonStore } from '@/store/modules/common' 101 | // 在此省略defineComponent 102 | setup(){ 103 | const commonStore = CommonStore() 104 | return ()=>( 105 |
{commonStore.userInfo}
106 | ) 107 | } 108 | ``` 109 | 使用computed获取 110 | ```js 111 | const userInfo = computed(() => commonStore.userInfo) 112 | ``` 113 | 使用Pinia提供的**storeToRefs** 114 | 115 | ```js 116 | import { storeToRefs } from 'pinia' 117 | import { CommonStore } from '@/store/modules/common' 118 | 119 | ... 120 | const commonStore = CommonStore() 121 | const { userInfo } = storeToRefs(commonStore) 122 | ``` 123 | ### 修改State 124 | 修改state的三种方式: 125 | 126 | 1. 直接修改(不推荐) 127 | 128 | ```js 129 | commonStore.userInfo = '曹操' 130 | ``` 131 | 2. 通过$patch 132 | 133 | ```js 134 | commonStore.$patch({ 135 | userInfo:'曹操' 136 | }) 137 | ``` 138 | 3. 通过actions修改store 139 | 140 | ```js 141 | export const CommonStore = defineStore('common', { 142 | // 状态库 143 | state: () => ({ 144 | userInfo: null, //用户信息 145 | }), 146 | actions: { 147 | setUserInfo(data) { 148 | this.userInfo = data 149 | }, 150 | }, 151 | }) 152 | ``` 153 | 154 | ```js 155 | import { CommonStore } from '@/store/modules/common' 156 | 157 | const commonStore = CommonStore() 158 | commonStore.setUserInfo('曹操') 159 | ``` 160 | ### Getters 161 | 162 | ```js 163 | export const CommonStore = defineStore('common', { 164 | // 状态库 165 | state: () => ({ 166 | userInfo: null, //用户信息 167 | }), 168 | getters: { 169 | getUserInfo: (state) => state.userInfo 170 | } 171 | }) 172 | ``` 173 | 使用同State获取 174 | 175 | ### Actions 176 | Pinia赋予了Actions更大的职能,相较于Vuex,Pinia去除了Mutations,仅依靠Actions来更改Store状态,同步异步都可以放在Actions中。 177 | 178 | #### 同步action 179 | 180 | ```js 181 | export const CommonStore = defineStore('common', { 182 | // 状态库 183 | state: () => ({ 184 | userInfo: null, //用户信息 185 | }), 186 | actions: { 187 | setUserInfo(data) { 188 | this.userInfo = data 189 | }, 190 | }, 191 | }) 192 | ``` 193 | #### 异步actions 194 | 195 | ```js 196 | ... 197 | actions: { 198 | async getUserInfo(params) { 199 | const data = await api.getUser(params) 200 | return data 201 | }, 202 | } 203 | ``` 204 | 205 | #### 内部actions间相互调用 206 | 207 | ```js 208 | ... 209 | actions: { 210 | async getUserInfo(params) { 211 | const data = await api.getUser(params) 212 | this.setUserInfo(data) 213 | return data 214 | }, 215 | setUserInfo(data){ 216 | this.userInfo = data 217 | } 218 | } 219 | ``` 220 | 221 | #### modules间actions相互调用 222 | 223 | ```js 224 | import { UserStore } from './modules/user' 225 | 226 | ... 227 | actions: { 228 | async getUserInfo(params) { 229 | const data = await api.getUser(params) 230 | const userStore = UserStore() 231 | userStore.setUserInfo(data) 232 | return data 233 | }, 234 | } 235 | ``` 236 | 237 | ### pinia-plugin-persist 插件实现数据持久化 238 | 239 | #### 安装 240 | ```js 241 | npm i pinia-plugin-persist --save 242 | ``` 243 | #### 使用 244 | 245 | ```js 246 | // src/store/index.ts 247 | 248 | import { createPinia } from 'pinia' 249 | import piniaPluginPersist from 'pinia-plugin-persist' 250 | 251 | const store = createPinia().use(piniaPluginPersist) 252 | 253 | export default store 254 | ``` 255 | 对应store中的使用 256 | 257 | ```js 258 | export const CommonStore = defineStore('common', { 259 | // 状态库 260 | state: () => ({ 261 | userInfo: null, //用户信息 262 | }), 263 | // 开启数据缓存 264 | persist: { 265 | enabled: true, 266 | strategies: [ 267 | { 268 | storage: localStorage, // 默认存储在sessionStorage里 269 | paths: ['userInfo'], // 指定存储state,不写则存储所有 270 | }, 271 | ], 272 | }, 273 | }) 274 | ``` 275 | 276 | ![WX20220224-151530.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d926043927f5451ab68a1b0cc9c6ecb7~tplv-k3u1fbpfcp-watermark.image?) 277 | 278 | ## Fetch 279 | 为了更好的支持TypeScript,统计Api请求,这里将axios进行二次封装 280 | 281 | 结构目录: 282 | 283 | ![WX20220224-155540@2x.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/86f3fb7b98ee4d43af26b2cc681b5e6c~tplv-k3u1fbpfcp-watermark.image?) 284 | ```js 285 | // src/utils/fetch.ts 286 | 287 | import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios' 288 | import { getToken } from './util' 289 | import { Modal } from 'ant-design-vue' 290 | import { Message, Notification } from '@/utils/resetMessage' 291 | 292 | // .env环境变量 293 | const BaseUrl = import.meta.env.VITE_API_BASE_URL as string 294 | 295 | // create an axios instance 296 | const service: AxiosInstance = axios.create({ 297 | baseURL: BaseUrl, // 正式环境 298 | timeout: 60 * 1000, 299 | headers: {}, 300 | }) 301 | 302 | /** 303 | * 请求拦截 304 | */ 305 | service.interceptors.request.use( 306 | (config: AxiosRequestConfig) => { 307 | config.headers.common.Authorization = getToken() // 请求头带上token 308 | config.headers.common.token = getToken() 309 | return config 310 | }, 311 | (error) => Promise.reject(error), 312 | ) 313 | 314 | /** 315 | * 响应拦截 316 | */ 317 | service.interceptors.response.use( 318 | (response: AxiosResponse) => { 319 | if (response.status == 201 || response.status == 200) { 320 | const { code, status, msg } = response.data 321 | if (code == 401) { 322 | Modal.warning({ 323 | title: 'token出错', 324 | content: 'token失效,请重新登录!', 325 | onOk: () => { 326 | sessionStorage.clear() 327 | }, 328 | }) 329 | } else if (code == 200) { 330 | if (status) { 331 | // 接口请求成功 332 | msg && Message.success(msg) // 后台如果返回了msg,则将msg提示出来 333 | return Promise.resolve(response) // 返回成功数据 334 | } 335 | // 接口异常 336 | msg && Message.warning(msg) // 后台如果返回了msg,则将msg提示出来 337 | return Promise.reject(response) // 返回异常数据 338 | } else { 339 | // 接口异常 340 | msg && Message.error(msg) 341 | return Promise.reject(response) 342 | } 343 | } 344 | return response 345 | }, 346 | (error) => { 347 | if (error.response.status) { 348 | switch (error.response.status) { 349 | case 500: 350 | Notification.error({ 351 | message: '温馨提示', 352 | description: '服务异常,请重启服务器!', 353 | }) 354 | break 355 | case 401: 356 | Notification.error({ 357 | message: '温馨提示', 358 | description: '服务异常,请重启服务器!', 359 | }) 360 | break 361 | case 403: 362 | Notification.error({ 363 | message: '温馨提示', 364 | description: '服务异常,请重启服务器!', 365 | }) 366 | break 367 | // 404请求不存在 368 | case 404: 369 | Notification.error({ 370 | message: '温馨提示', 371 | description: '服务异常,请重启服务器!', 372 | }) 373 | break 374 | default: 375 | Notification.error({ 376 | message: '温馨提示', 377 | description: '服务异常,请重启服务器!', 378 | }) 379 | } 380 | } 381 | return Promise.reject(error.response) 382 | }, 383 | ) 384 | 385 | interface Http { 386 | fetch(params: AxiosRequestConfig): Promise> 387 | } 388 | 389 | const http: Http = { 390 | // 用法与axios一致(包含axios内置所有请求方式) 391 | fetch(params) { 392 | return new Promise((resolve, reject) => { 393 | service(params) 394 | .then((res) => { 395 | resolve(res.data) 396 | }) 397 | .catch((err) => { 398 | reject(err.data) 399 | }) 400 | }) 401 | }, 402 | } 403 | 404 | export default http['fetch'] 405 | 406 | ``` 407 | ### 使用 408 | 409 | ```js 410 | // src/api/user.ts 411 | 412 | import qs from 'qs' 413 | import fetch from '@/utils/fetch' 414 | import { IUserApi } from './types/user' 415 | 416 | const UserApi: IUserApi = { 417 | // 登录 418 | login: (params) => { 419 | return fetch({ 420 | method: 'post', 421 | url: '/login', 422 | data: params, 423 | }) 424 | } 425 | } 426 | 427 | export default UserApi 428 | 429 | ``` 430 | 431 | ### 类型定义 432 | 433 | ```js 434 | /** 435 | * 接口返回结果Types 436 | * -------------------------------------------------------------------------- 437 | */ 438 | // 登录返回结果 439 | export interface ILoginData { 440 | token: string 441 | userInfo: { 442 | address: string 443 | username: string 444 | } 445 | } 446 | 447 | /** 448 | * 接口参数Types 449 | * -------------------------------------------------------------------------- 450 | */ 451 | // 登录参数 452 | export interface ILoginApiParams { 453 | username: string // 用户名 454 | password: string // 密码 455 | captcha: string // 验证码 456 | uuid: string // 验证码uuid 457 | } 458 | 459 | /** 460 | * 接口定义Types 461 | * -------------------------------------------------------------------------- 462 | */ 463 | export interface IUserApi { 464 | login: (params: ILoginApiParams) => Promise> 465 | } 466 | 467 | ``` 468 | 469 | ## 国际化配置 470 | 471 | ### 安装 472 | 引入i18n依赖包,注意vue3中配置i18n需要安装 [V9+版本](https://github.com/intlify/vue-i18n-next)、 473 | 474 | ``` 475 | npm install vue-i18n@9 476 | ``` 477 | ### 配置 478 | 该脚手架i18n采用模块化的设计思路: 479 | >`src/i18n/model`对应不同模块的国际化文件(根据实际业务创建),`src/i18n/lang`对应不同语言包(集成所有模块的语言定义),`src/i18n/index.ts`创建i18n实例,并导出。 480 | ```js 481 | //src/model/menu.ts 482 | export default { 483 | zh: { 484 | userManage: '用户管理' 485 | }, 486 | en: { 487 | userManage: 'User Manage' 488 | } 489 | } 490 | ``` 491 | 492 | ```js 493 | //src/lang/en_US.ts 494 | import Menu from '../model/menu' 495 | 496 | export default { 497 | menu: Menu.en 498 | } 499 | ``` 500 | 501 | ```js 502 | // src/i18n/index.ts 503 | import { createI18n } from 'vue-i18n' 504 | 505 | import zh_CN from './lang/zh_CN' 506 | import en from './lang/en_US' 507 | 508 | const localLang = localStorage.getItem('localLang') 509 | 510 | const i18n = createI18n({ 511 | legacy: false, 512 | globalInjection: true, // 全局模式,可以直接使用 $t 513 | locale: localLang as string, 514 | messages: { 515 | 'zh-cn': zh_CN, // 中文语言包 516 | en: en // 英文语言包 517 | } 518 | }) 519 | 520 | export default i18n 521 | ``` 522 | 其中 `createI18n`配置项中: 523 | - `legacy`:默认值为false,当使用 Composition API 时需要设置成true,否则会报以下类型错误: 524 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32a70692d65f442780c31ff48dfccba0~tplv-k3u1fbpfcp-watermark.image?) 525 | - `globalInjection`: 默认值为true, true: 可以直接使用 `$t`声明,如$t('menu.authManage');false: 通过局部组件单独引入的形式,下面会提到。 526 | - `locale`: 当前展示的语言,**ps:需注意与messages定义的key名称对应** 527 | - `messages`: 不同语言对应的语言包集合 528 | 529 | ### 在main.ts中引入 530 | 531 | ``` 532 | // src/main.ts 533 | ... 534 | import i18n from '@/i18n' 535 | app.use(i18n) 536 | ... 537 | ``` 538 | ### 如何在文件中使用 539 | 上文说到在创建`createI18n`实例时有一个`globalInjection`配置项,配置的不同,使用方式也不同,具体如下: 540 | - globalInjection:false 541 | 542 | ``` 543 | 546 | 551 | ``` 552 | - globalInjection:true 553 | ``` 554 | 557 | 559 | ``` 560 | ### 切换语言 561 | 562 | 在App.ts中`provide`一个切换语言的方法`changeLang`,主要代码如下: 563 | 564 | ```js 565 | //src/App.ts 566 | ... 567 | import { provide } from 'vue' 568 | import { useI18n } from 'vue-i18n' 569 | const { locale } = useI18n() 570 | // setup 571 | const changeLang = (lang: string) => { 572 | locale['value'] = lang 573 | localStorage.setItem('localLang', lang) 574 | } 575 | ``` 576 | 调用`changeLang`方法 577 | 578 | ```js 579 | import { inject } from 'vue' 580 | // 注入切换语言方法 581 | const changeLang = inject('changeLang') 582 | 583 |
584 | 585 | 中文 586 | 英文 587 | 588 |
589 | ``` 590 | 591 | ## Router4 592 | 1. 基础路由 593 | ```js 594 | // src/router/router.config.ts 595 | 596 | const Routes: Array = [ 597 | { 598 | path: '/403', 599 | name: '403', 600 | component: () => 601 | import(/* webpackChunkName: "403" */ '@/views/exception/403'), 602 | meta: { title: '403', permission: ['exception'], hidden: true }, 603 | }, 604 | { 605 | path: '/404', 606 | name: '404', 607 | component: () => 608 | import(/* webpackChunkName: "404" */ '@/views/exception/404'), 609 | meta: { title: '404', permission: ['exception'], hidden: true }, 610 | }, 611 | { 612 | path: '/500', 613 | name: '500', 614 | component: () => 615 | import(/* webpackChunkName: "500" */ '@/views/exception/500'), 616 | meta: { title: '500', permission: ['exception'], hidden: true }, 617 | }, 618 | { 619 | path: '/:pathMatch(.*)', 620 | name: 'error', 621 | component: () => 622 | import(/* webpackChunkName: "404" */ '@/views/exception/404'), 623 | meta: { title: '404', hidden: true }, 624 | }, 625 | ] 626 | ``` 627 | > title: 导航显示文字;hidden: 导航上是否隐藏该路由 (true: 不显示 false:显示) 628 | 2. 动态路由(权限路由) 629 | 630 | ```js 631 | // src/router/router.ts 632 | 633 | router.beforeEach( 634 | async ( 635 | to: RouteLocationNormalized, 636 | from: RouteLocationNormalized, 637 | next: NavigationGuardNext, 638 | ) => { 639 | const token: string = getToken() as string 640 | if (token) { 641 | // 第一次加载路由列表并且该项目需要动态路由 642 | if (!isAddDynamicMenuRoutes) { 643 | try { 644 | //获取动态路由表 645 | const res: any = await UserApi.getPermissionsList({}) 646 | if (res.code == 200) { 647 | isAddDynamicMenuRoutes = true 648 | const menu = res.data 649 | // 通过路由表生成标准格式路由 650 | const menuRoutes: any = fnAddDynamicMenuRoutes( 651 | menu.menuList || [], 652 | [], 653 | ) 654 | mainRoutes.children = [] 655 | mainRoutes.children?.unshift(...menuRoutes, ...Routes) 656 | // 动态添加路由 657 | router.addRoute(mainRoutes) 658 | // 注:这步很关键,不然导航获取不到路由 659 | router.options.routes.unshift(mainRoutes) 660 | // 本地存储按钮权限集合 661 | sessionStorage.setItem( 662 | 'permissions', 663 | JSON.stringify(menu.permissions || '[]'), 664 | ) 665 | if (to.path == '/' || to.path == '/login') { 666 | const firstName = menuRoutes.length && menuRoutes[0].name 667 | next({ name: firstName, replace: true }) 668 | } else { 669 | next({ path: to.fullPath }) 670 | } 671 | } else { 672 | sessionStorage.setItem('menuList', '[]') 673 | sessionStorage.setItem('permissions', '[]') 674 | next() 675 | } 676 | } catch (error) { 677 | console.log( 678 | `%c${error} 请求菜单列表和权限失败,跳转至登录页!!`, 679 | 'color:orange', 680 | ) 681 | } 682 | } else { 683 | if (to.path == '/' || to.path == '/login') { 684 | next(from) 685 | } else { 686 | next() 687 | } 688 | } 689 | } else { 690 | isAddDynamicMenuRoutes = false 691 | if (to.name != 'login') { 692 | next({ name: 'login' }) 693 | } 694 | next() 695 | } 696 | }, 697 | ) 698 | ``` 699 | ## Layouts布局组件 700 | > 脚手架提供多种排版布局,目录结构如下: 701 | 702 | ![layout.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/073048ec7d6447c2939c74c5ccf69d79~tplv-k3u1fbpfcp-watermark.image?) 703 | - BlankLayout.tsx: 空白布局,只做路由分发 704 | - RouteLayout.tsx: 主体布局,内容显示部分,包含面包屑 705 | - LevelBasicLayout.tsx 多级展示布局,适用于2级以上路由 706 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/39d23619aa004b1ea0a4da7884517398~tplv-k3u1fbpfcp-watermark.image?) 707 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/82e3f3a57ba04cdaabe2b4e616f7459a~tplv-k3u1fbpfcp-watermark.image?) 708 | - SimplifyBasicLayout.tsx 简化版多级展示布局,适用于2级以上路由 709 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/84f3a4f14e874488a138ca985d811f8d~tplv-k3u1fbpfcp-watermark.image?) 710 | 711 | ## 相关参考链接 712 | - [Pinia官网](https://pinia.vuejs.org/) 713 | - [Vue3官网](https://v3.cn.vuejs.org/guide/introduction.html) 714 | - [Vite](https://vitejs.cn/) 715 | - [Antd Design Vue](https://2x.antdv.com/components/overview-cn/) 716 | 717 | ## 最后 718 | 文章暂时就写到这,后续会增加JSX语法部分,如果本文对您有什么帮助,别忘了动动手指点个赞❤️。 719 | 本文如果有错误和不足之处,欢迎大家在评论区指出,多多提出您宝贵的意见! 720 | 721 | 最后分享本脚手架地址:[github地址](https://github.com/sunshine824/vue3.0-typescript-starter/tree/master)、 722 | [gitee地址](https://gitee.com/sunshine824/vue3.0-typescript-starter/tree/master/) 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vue-tsc --noEmit && vite build", 6 | "serve": "vite preview" 7 | }, 8 | "dependencies": { 9 | "ant-design-vue": "2.2.2", 10 | "axios": "^0.21.1", 11 | "codemirror": "^5.62.2", 12 | "crypto-js": "^4.0.0", 13 | "qs": "^6.10.1", 14 | "spark-md5": "^3.0.1", 15 | "vue": "^3.2.30", 16 | "vue-router": "^4.0.9", 17 | "vuex": "^4.0.1" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^16.4.3", 21 | "@typescript-eslint/parser": "^4.27.0", 22 | "@vitejs/plugin-vue": "^1.2.3", 23 | "@vitejs/plugin-vue-jsx": "^1.1.3", 24 | "@vue/compiler-sfc": "^3.0.5", 25 | "eslint": "^7.28.0", 26 | "eslint-config-prettier": "^8.3.0", 27 | "eslint-plugin-prettier": "^3.4.0", 28 | "eslint-plugin-vue": "^7.11.1", 29 | "less": "^4.1.1", 30 | "less-loader": "^10.0.0", 31 | "mockjs": "^1.1.0", 32 | "prettier": "^2.3.1", 33 | "typescript": "^4.3.2", 34 | "vite": "^2.3.7", 35 | "vite-plugin-compression": "^0.4.0", 36 | "vite-plugin-style-import": "^1.0.1", 37 | "vue-tsc": "^0.0.24" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/public/favicon.ico -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref } from 'vue' 2 | import { RouterView } from 'vue-router' 3 | import { ConfigProvider } from 'ant-design-vue' 4 | import zhCN from 'ant-design-vue/es/locale/zh_CN' 5 | 6 | export default defineComponent({ 7 | name: 'App', 8 | components: { RouterView }, 9 | setup() { 10 | const locale = zhCN 11 | return () => ( 12 | 13 | 14 | 15 | ) 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/api/types/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 接口返回结果Types 3 | * ------------------------------------------------------------------------------------------ 4 | */ 5 | // 登录返回结果 6 | export interface ILoginData { 7 | token: string 8 | userInfo: { 9 | address: string 10 | username: string 11 | } 12 | } 13 | 14 | /** 15 | * 接口参数Types 16 | * ------------------------------------------------------------------------------------------ 17 | */ 18 | // 登录参数 19 | export interface ILoginApiParams { 20 | username: string // 用户名 21 | password: string // 密码 22 | captcha: string // 验证码 23 | uuid: string // 验证码uuid 24 | } 25 | // 注销登录参数 26 | export interface ILogoutParams {} 27 | // 获取用户权限参数 28 | export interface IPermissionsParams {} 29 | 30 | /** 31 | * 接口定义Types 32 | * ------------------------------------------------------------------------------------------ 33 | */ 34 | export interface IUserApi { 35 | login: (params: ILoginApiParams) => Promise> 36 | logout: (params: ILogoutParams) => Promise> 37 | getPermissionsList: ( 38 | params: IPermissionsParams, 39 | ) => Promise> 40 | } 41 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs' 2 | import fetch from '@/utils/fetch' 3 | import { IUserApi } from './types/user' 4 | 5 | const UserApi: IUserApi = { 6 | // 登录 7 | login: (params) => { 8 | return fetch({ 9 | method: 'post', 10 | url: '/login', 11 | data: params, 12 | }) 13 | }, 14 | // 注销登录 15 | logout: (params) => { 16 | return fetch({ 17 | method: 'get', 18 | url: '/logout', 19 | data: params, 20 | }) 21 | }, 22 | // 获取权限列表 23 | getPermissionsList: (params) => { 24 | return fetch({ 25 | method: 'get', 26 | url: '/navPerson', 27 | data: qs.stringify(params), 28 | }) 29 | }, 30 | } 31 | 32 | export default UserApi 33 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/screenshot/screen01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/assets/screenshot/screen01.png -------------------------------------------------------------------------------- /src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/assets/user.png -------------------------------------------------------------------------------- /src/components/ABreadCrumb/index.module.less: -------------------------------------------------------------------------------- 1 | .bread-crumb { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | align-items: center; 5 | .location { 6 | font-size: 13px; 7 | color: rgba(0, 0, 0, 0.8); 8 | margin-right: 5px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ABreadCrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, watch, Ref, ref, onMounted } from 'vue' 2 | import { useRoute, useRouter, RouteRecordRaw } from 'vue-router' 3 | import { Breadcrumb } from 'ant-design-vue' 4 | 5 | import styles from './index.module.less' 6 | 7 | const ABreadCrumb = defineComponent({ 8 | name: 'ABreadCrumb', 9 | setup(props) { 10 | const route = useRoute() 11 | const router = useRouter() 12 | 13 | const breadList: Ref = ref([]) 14 | 15 | onMounted(() => { 16 | getRouteBreadList() 17 | }) 18 | 19 | // 监听路由变化 20 | watch(route, (val) => { 21 | breadList['value'] = [] 22 | getRouteBreadList() 23 | }) 24 | 25 | // 获取路由地址列表 26 | const getRouteBreadList = () => { 27 | const paths = route.path.split('/') 28 | if (paths.length > 1) { 29 | const menus = getMenus().filter( 30 | (item: RouteRecordRaw) => item.name == paths[1], 31 | ) 32 | if (menus.length) { 33 | getRouteDict(menus, paths) 34 | } 35 | } 36 | } 37 | 38 | // 根据路由获取面包屑列表 39 | const getRouteDict = (menus: any, paths: string[]) => { 40 | menus.forEach((item: any) => { 41 | if (paths.includes(item.name)) { 42 | breadList['value'].push(item?.meta?.title) 43 | } 44 | if (item.children && item.children.length) { 45 | getRouteDict(item.children, paths) 46 | } 47 | }) 48 | } 49 | 50 | // 获取路由列表 51 | const getMenus = () => { 52 | let menuList: RouteRecordRaw[] = [] 53 | const routes: Array = router.options?.routes || [] 54 | routes.forEach((item) => { 55 | if (item.path == '/') { 56 | menuList = item.children || [] 57 | } 58 | }) 59 | return JSON.parse(JSON.stringify(menuList)) 60 | } 61 | 62 | return () => ( 63 |
64 | 当前位置: 65 | 66 | {breadList['value'].map((item) => { 67 | return {item} 68 | })} 69 | 70 |
71 | ) 72 | }, 73 | }) 74 | 75 | export default ABreadCrumb 76 | -------------------------------------------------------------------------------- /src/components/Exception/index.module.less: -------------------------------------------------------------------------------- 1 | .exception { 2 | display: flex; 3 | align-items: center; 4 | height: 80%; 5 | min-height: 500px; 6 | 7 | .imgBlock { 8 | flex: 0 0 62.5%; 9 | width: 62.5%; 10 | padding-right: 152px; 11 | zoom: 1; 12 | &::before, 13 | &::after { 14 | content: ' '; 15 | display: table; 16 | } 17 | &::after { 18 | clear: both; 19 | height: 0; 20 | font-size: 0; 21 | visibility: hidden; 22 | } 23 | } 24 | 25 | .imgEle { 26 | float: right; 27 | width: 100%; 28 | max-width: 430px; 29 | height: 360px; 30 | background-repeat: no-repeat; 31 | background-position: 50% 50%; 32 | background-size: contain; 33 | } 34 | 35 | .content { 36 | flex: auto; 37 | 38 | h1 { 39 | margin-bottom: 24px; 40 | color: #434e59; 41 | font-weight: 600; 42 | font-size: 72px; 43 | line-height: 72px; 44 | } 45 | 46 | .desc { 47 | margin-bottom: 16px; 48 | color: rgba(0, 0, 0, 0.45); 49 | font-size: 20px; 50 | line-height: 28px; 51 | } 52 | 53 | .actions { 54 | button:not(:last-child) { 55 | margin-right: 8px; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Exception/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import Types from './type' 3 | 4 | import styles from './index.module.less' 5 | 6 | const ExceptionPage = defineComponent({ 7 | name: 'ExceptionPage', 8 | props: { 9 | type: { 10 | type: String, 11 | default: '404', 12 | }, 13 | }, 14 | setup(props) { 15 | return () => ( 16 |
17 |
18 |
22 |
23 |
24 |

{Types[props.type].title}

25 |
{Types[props.type].desc}
26 |
27 |
28 | ) 29 | }, 30 | }) 31 | 32 | export default ExceptionPage 33 | -------------------------------------------------------------------------------- /src/components/Exception/type.ts: -------------------------------------------------------------------------------- 1 | const types: any = { 2 | 403: { 3 | img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg', 4 | title: '403', 5 | desc: '抱歉,你无权访问该页面', 6 | }, 7 | 404: { 8 | img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg', 9 | title: '404', 10 | desc: '抱歉,你访问的页面不存在或仍在开发中', 11 | }, 12 | 500: { 13 | img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg', 14 | title: '500', 15 | desc: '抱歉,服务器出错了', 16 | }, 17 | }; 18 | 19 | export default types; 20 | -------------------------------------------------------------------------------- /src/components/GlobalBg/index.module.less: -------------------------------------------------------------------------------- 1 | .main-box { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | overflow: hidden; 8 | display: flex; 9 | width: 100%; 10 | height: 100vh; 11 | background-color: rgba(0, 0, 0, 1); 12 | overflow: hidden; 13 | :global { 14 | #bg { 15 | position: fixed; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/GlobalBg/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, onMounted } from 'vue' 2 | 3 | import styles from './index.module.less' 4 | 5 | const GlobalBg = defineComponent({ 6 | name: 'GlobalBg', 7 | setup(props, { slots }) { 8 | onMounted(() => { 9 | initBg() 10 | }) 11 | // 初始化背景 12 | const initBg = () => { 13 | const STAR_COUNT = (window.innerWidth + window.innerHeight) / 8, 14 | STAR_SIZE = 3, 15 | STAR_MIN_SCALE = 0.2, 16 | OVERFLOW_THRESHOLD = 50 17 | 18 | const canvas = document.querySelector('#bg'), 19 | context = (canvas as any).getContext('2d') 20 | 21 | let scale = 1, // device pixel ratio 22 | width: any, 23 | height: any 24 | 25 | let stars: any[] = [] 26 | 27 | let pointerX: any, pointerY: any 28 | 29 | let velocity = { x: 0, y: 0, tx: 0, ty: 0, z: 0.0005 } 30 | 31 | let touchInput = false 32 | 33 | generate() 34 | resize() 35 | step() 36 | 37 | window.onresize = resize 38 | 39 | function generate() { 40 | for (let i = 0; i < STAR_COUNT; i++) { 41 | stars.push({ 42 | x: 0, 43 | y: 0, 44 | z: STAR_MIN_SCALE + Math.random() * (1 - STAR_MIN_SCALE), 45 | }) 46 | } 47 | } 48 | 49 | function placeStar(star: any) { 50 | star.x = Math.random() * width 51 | star.y = Math.random() * height 52 | } 53 | 54 | function recycleStar(star: any) { 55 | let direction = 'z' 56 | 57 | let vx = Math.abs(velocity.x), 58 | vy = Math.abs(velocity.y) 59 | 60 | if (vx > 1 || vy > 1) { 61 | let axis 62 | 63 | if (vx > vy) { 64 | axis = Math.random() < vx / (vx + vy) ? 'h' : 'v' 65 | } else { 66 | axis = Math.random() < vy / (vx + vy) ? 'v' : 'h' 67 | } 68 | 69 | if (axis === 'h') { 70 | direction = velocity.x > 0 ? 'l' : 'r' 71 | } else { 72 | direction = velocity.y > 0 ? 't' : 'b' 73 | } 74 | } 75 | 76 | star.z = STAR_MIN_SCALE + Math.random() * (1 - STAR_MIN_SCALE) 77 | 78 | if (direction === 'z') { 79 | star.z = 0.1 80 | star.x = Math.random() * width 81 | star.y = Math.random() * height 82 | } else if (direction === 'l') { 83 | star.x = -OVERFLOW_THRESHOLD 84 | star.y = height * Math.random() 85 | } else if (direction === 'r') { 86 | star.x = width + OVERFLOW_THRESHOLD 87 | star.y = height * Math.random() 88 | } else if (direction === 't') { 89 | star.x = width * Math.random() 90 | star.y = -OVERFLOW_THRESHOLD 91 | } else if (direction === 'b') { 92 | star.x = width * Math.random() 93 | star.y = height + OVERFLOW_THRESHOLD 94 | } 95 | } 96 | 97 | function resize() { 98 | scale = window.devicePixelRatio || 1 99 | 100 | width = window.innerWidth * scale 101 | height = window.innerHeight * scale 102 | ;(canvas as any).width = width 103 | ;(canvas as any).height = height 104 | 105 | stars.forEach(placeStar) 106 | } 107 | 108 | function step() { 109 | context.clearRect(0, 0, width, height) 110 | 111 | update() 112 | render() 113 | 114 | requestAnimationFrame(step) 115 | } 116 | 117 | function update() { 118 | velocity.tx *= 0.96 119 | velocity.ty *= 0.96 120 | 121 | velocity.x += (velocity.tx - velocity.x) * 0.8 122 | velocity.y += (velocity.ty - velocity.y) * 0.8 123 | 124 | stars.forEach((star) => { 125 | star.x += velocity.x * star.z 126 | star.y += velocity.y * star.z 127 | 128 | star.x += (star.x - width / 2) * velocity.z * star.z 129 | star.y += (star.y - height / 2) * velocity.z * star.z 130 | star.z += velocity.z 131 | 132 | // recycle when out of bounds 133 | if ( 134 | star.x < -OVERFLOW_THRESHOLD || 135 | star.x > width + OVERFLOW_THRESHOLD || 136 | star.y < -OVERFLOW_THRESHOLD || 137 | star.y > height + OVERFLOW_THRESHOLD 138 | ) { 139 | recycleStar(star) 140 | } 141 | }) 142 | } 143 | 144 | function render() { 145 | stars.forEach((star) => { 146 | context.beginPath() 147 | context.lineCap = 'round' 148 | context.lineWidth = STAR_SIZE * star.z * scale 149 | context.strokeStyle = 150 | 'rgba(255,255,255,' + (0.5 + 0.5 * Math.random()) + ')' 151 | 152 | context.beginPath() 153 | context.moveTo(star.x, star.y) 154 | 155 | var tailX = velocity.x * 2, 156 | tailY = velocity.y * 2 157 | 158 | // stroke() wont work on an invisible line 159 | if (Math.abs(tailX) < 0.1) tailX = 0.5 160 | if (Math.abs(tailY) < 0.1) tailY = 0.5 161 | 162 | context.lineTo(star.x + tailX, star.y + tailY) 163 | 164 | context.stroke() 165 | }) 166 | } 167 | 168 | function movePointer(x: any, y: any) { 169 | if (typeof pointerX === 'number' && typeof pointerY === 'number') { 170 | let ox = x - pointerX, 171 | oy = y - pointerY 172 | 173 | velocity.tx = velocity.tx + (ox / 8) * scale * (touchInput ? 1 : -1) 174 | velocity.ty = velocity.ty + (oy / 8) * scale * (touchInput ? 1 : -1) 175 | } 176 | 177 | pointerX = x 178 | pointerY = y 179 | } 180 | } 181 | return () => ( 182 |
183 | 184 | {slots.content?.()} 185 | {slots.footer?.()} 186 |
187 | ) 188 | }, 189 | }) 190 | 191 | export default GlobalBg 192 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/GlobalHeader.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { Layout } from 'ant-design-vue' 3 | 4 | import styles from './index.module.less' 5 | import Logo from '../../assets/logo.png' 6 | 7 | const GlobalHeader = defineComponent({ 8 | name: 'GlobalHeader', 9 | setup(props, { slots }) { 10 | return () => ( 11 |
12 | 13 | 17 |
{slots.content?.()}
18 |
19 |
20 | ) 21 | }, 22 | }) 23 | 24 | export default GlobalHeader 25 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/LevelMenus.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, onMounted, ref, watch } from 'vue' 2 | import { RouteRecordRaw, useRoute, useRouter } from 'vue-router' 3 | import router from '../../router' 4 | 5 | import styles from './index.module.less' 6 | 7 | const LevelMenus = defineComponent({ 8 | name: 'LevelMenus', 9 | props: { 10 | menuLists: { 11 | type: Array, 12 | default: [] 13 | } 14 | }, 15 | setup(props, { slots }) { 16 | const route = useRoute() // 路由实例 17 | 18 | const router = useRouter() 19 | 20 | const baseUrl = ref() // 当前跳转基础路由 21 | 22 | onMounted(() => { 23 | const paths = route.path.split('/') 24 | if (paths.length > 1) { 25 | baseUrl.value = '/' + paths[1] 26 | } 27 | }) 28 | 29 | // 路由跳转 30 | const skipTo = (item: RouteRecordRaw) => { 31 | router.push(item.path) 32 | } 33 | 34 | // 监听路由变化 35 | watch(route, val => { 36 | const paths = val.path.split('/') 37 | if (paths.length > 1) { 38 | baseUrl.value = '/' + paths[1] 39 | } 40 | }) 41 | 42 | return () => ( 43 |
44 |
    45 | {(props.menuLists as RouteRecordRaw[]).map(menu => { 46 | return ( 47 |
  • 48 | 49 | {menu?.meta?.title} 50 |
  • 51 | ) 52 | })} 53 |
54 |
55 | ) 56 | } 57 | }) 58 | 59 | export default LevelMenus 60 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/Menus.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, watch, computed, onMounted } from 'vue' 2 | import { useRoute, useRouter, RouteRecordRaw } from 'vue-router' 3 | import { Menu } from 'ant-design-vue' 4 | 5 | import styles from './index.module.less' 6 | 7 | const Menus = defineComponent({ 8 | name: 'Menus', 9 | props: { 10 | mode: { 11 | type: String, 12 | default: 'horizontal', 13 | }, 14 | menuLists: { 15 | type: Array, 16 | default: [], 17 | }, 18 | }, 19 | setup(props, { slots }) { 20 | const route = useRoute() 21 | const router = useRouter() 22 | const activeRoute = ref() // 当前路由 23 | 24 | onMounted(() => { 25 | activeRoute.value = route.path 26 | }) 27 | 28 | // 默认展开项 29 | const openKeys = computed(() => { 30 | if (props.mode == 'inline') { 31 | return ['/designCenter/materialMange'] 32 | } 33 | return [] 34 | }) 35 | 36 | // 路由跳转 37 | const handleMenuClick = (item: { key: string }) => { 38 | router.push(item.key) 39 | } 40 | 41 | // 监听路由变化 42 | watch(route, (val) => { 43 | activeRoute.value = val.path 44 | }) 45 | 46 | // 子级导航渲染 47 | const SubItem = (menus: any) => { 48 | return ( 49 | 50 | {{ 51 | title: () => ( 52 | <> 53 | {menus.meta?.icon && ( 54 | 55 | )} 56 | {menus.meta?.title} 57 | 58 | ), 59 | default: () => ( 60 | <> 61 | {menus.children.map((menu: any) => { 62 | if (!menu.children || !menu.children.length) { 63 | return ( 64 | 65 | {menus.meta?.icon && ( 66 | 69 | )} 70 | {menu.meta?.title} 71 | 72 | ) 73 | } else { 74 | return SubItem(menu) 75 | } 76 | })} 77 | 78 | ), 79 | }} 80 | 81 | ) 82 | } 83 | 84 | return () => ( 85 |
88 | 97 | {(props.menuLists as RouteRecordRaw[]).map((menu) => { 98 | if (!menu.children || !menu.children.length) { 99 | return ( 100 | 101 | {menu.meta?.icon && ( 102 | 103 | )} 104 | {menu.meta?.title} 105 | 106 | ) 107 | } else { 108 | return SubItem(menu) 109 | } 110 | })} 111 | 112 |
113 | ) 114 | }, 115 | }) 116 | 117 | export default Menus 118 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.module.less: -------------------------------------------------------------------------------- 1 | .header-animat { 2 | position: relative; 3 | z-index: 100; 4 | .right-con { 5 | display: flex; 6 | flex-flow: row nowrap; 7 | align-items: center; 8 | } 9 | :global { 10 | .ant-layout-header { 11 | background-color: #191c3e; 12 | display: flex; 13 | flex-flow: row nowrap; 14 | justify-content: space-between; 15 | align-items: center; 16 | height: 54px; 17 | padding: 0 40px 0 15px; 18 | border-bottom: 1px solid #434343; 19 | .logo { 20 | min-width: 120px; 21 | height: 100%; 22 | cursor: pointer; 23 | display: flex; 24 | flex-flow: row nowrap; 25 | align-items: center; 26 | img { 27 | height: 80%; 28 | margin-right: 10px; 29 | } 30 | .txt { 31 | font-size: 20px; 32 | color: #fff; 33 | font-weight: bold; 34 | letter-spacing: 3px; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | .menu-class { 42 | :global { 43 | .menu-icon { 44 | margin-right: 10px; 45 | } 46 | } 47 | .ant-menu-inline-collapsed { 48 | width: 60px; 49 | & > .ant-menu-submenu { 50 | & > .ant-menu-submenu-title { 51 | padding: 0 24px !important; 52 | .menu-icon { 53 | & + span { 54 | opacity: 0; 55 | } 56 | } 57 | } 58 | } 59 | & > .ant-menu-item { 60 | padding: 0 24px !important; 61 | .menu-icon { 62 | & + span { 63 | opacity: 0; 64 | } 65 | } 66 | } 67 | } 68 | &.horizontal-menu-class { 69 | :global { 70 | .ant-menu { 71 | background-color: #19192e; 72 | max-width: 700px; 73 | margin-right: 20px; 74 | .ant-menu-item { 75 | color: #b0b0b1; 76 | font-size: 15px; 77 | border-bottom: none; 78 | top: 0; 79 | &:hover { 80 | background-color: rgba(255, 255, 255, 0); 81 | } 82 | &-active { 83 | color: rgba(255, 255, 255, 0.9); 84 | background-color: rgba(255, 255, 255, 0.7); 85 | } 86 | &-selected { 87 | background-color: rgba(54, 54, 80, 0.8) !important; 88 | position: relative; 89 | font-weight: 700; 90 | color: rgba(255, 255, 255, 0.9); 91 | border-bottom: none; 92 | margin: 0; 93 | height: 55px; 94 | line-height: 55px; 95 | &::after { 96 | content: ''; 97 | position: absolute; 98 | bottom: 0; 99 | height: 4px; 100 | width: 100%; 101 | background: #0972fe; 102 | left: 0; 103 | z-index: 111; 104 | opacity: 1; 105 | transform: scaleY(1); 106 | top: auto; 107 | } 108 | } 109 | } 110 | } 111 | .ant-menu-submenu { 112 | top: 0; 113 | border-bottom: none; 114 | &-title { 115 | color: #b0b0b1; 116 | font-size: 15px; 117 | &:hover { 118 | color: #fff; 119 | } 120 | } 121 | &-active { 122 | color: rgba(255, 255, 255, 0.9); 123 | border-bottom: none !important; 124 | } 125 | &-selected { 126 | background: #4a4b4c; 127 | position: relative; 128 | font-weight: 700; 129 | color: rgba(255, 255, 255, 0.9); 130 | border-bottom: none !important; 131 | .ant-menu-submenu-title { 132 | color: #fff; 133 | } 134 | &::after { 135 | content: ''; 136 | position: absolute; 137 | bottom: 0; 138 | height: 3px; 139 | width: 100%; 140 | background: #0972fe; 141 | left: 0; 142 | } 143 | } 144 | } 145 | } 146 | } 147 | &.inline-menu-class { 148 | :global { 149 | .ant-menu-dark, 150 | .ant-menu-dark .ant-menu-sub { 151 | background: #1d1e1f; 152 | } 153 | .ant-menu-submenu:hover 154 | > .ant-menu-submenu-title 155 | > .ant-menu-submenu-expand-icon, 156 | .ant-menu-submenu:hover 157 | > .ant-menu-submenu-title 158 | > .ant-menu-submenu-arrow { 159 | color: #222b65; 160 | } 161 | .ant-menu-light .ant-menu-item:hover, 162 | .ant-menu-light .ant-menu-item-active, 163 | .ant-menu-light .ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open, 164 | .ant-menu-light .ant-menu-submenu-active, 165 | .ant-menu-light .ant-menu-submenu-title:hover { 166 | color: #222b65; 167 | } 168 | .ant-menu-dark .ant-menu-inline.ant-menu-sub { 169 | background: #000c17; 170 | } 171 | .ant-menu-submenu-selected { 172 | color: #222b65; 173 | } 174 | .ant-menu-item { 175 | font-size: 14px; 176 | border-bottom: none; 177 | top: 0; 178 | &:active { 179 | background: rgba(39, 60, 192, 0.05); 180 | } 181 | &:hover { 182 | background: rgba(39, 60, 192, 0.05); 183 | color: rgba(0, 0, 0, 0.85); 184 | } 185 | &-active { 186 | background: rgba(39, 60, 192, 0.05); 187 | color: rgba(0, 0, 0, 0.85); 188 | } 189 | &-selected { 190 | position: relative; 191 | color: #273cc0; 192 | background: rgba(39, 60, 192, 0.16) !important; 193 | &::after { 194 | border-color: #273cc0; 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | .level-menus { 203 | height: 54px; 204 | max-width: 800px; 205 | margin-right: 20px; 206 | .level-menus-list { 207 | display: flex; 208 | flex-flow: row nowrap; 209 | align-items: center; 210 | height: 100%; 211 | margin-bottom: 0; 212 | li { 213 | height: 100%; 214 | display: flex; 215 | flex-flow: row nowrap; 216 | align-items: center; 217 | color: #b0b0b1; 218 | font-size: 15px; 219 | padding: 0 15px; 220 | position: relative; 221 | cursor: pointer; 222 | margin: 0 5px; 223 | &:hover { 224 | color: #fff; 225 | } 226 | :global { 227 | .menu-icon { 228 | margin-right: 15px; 229 | } 230 | } 231 | &::after { 232 | width: 100%; 233 | content: ''; 234 | position: absolute; 235 | height: 0px; 236 | bottom: 0; 237 | left: 0; 238 | background: #0972fe; 239 | } 240 | &.active { 241 | background-color: rgba(54, 54, 80, 0.8) !important; 242 | color: #fff; 243 | &::after { 244 | height: 3px; 245 | } 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import GlobalHeader from './GlobalHeader' 2 | import Menus from './Menus' 3 | import LevelMenus from './LevelMenus' 4 | 5 | export { GlobalHeader, Menus, LevelMenus } 6 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouterView } from 'vue-router' 3 | 4 | const BlankLayout = defineComponent({ 5 | setup() { 6 | return () => 7 | }, 8 | }) 9 | 10 | export default BlankLayout 11 | -------------------------------------------------------------------------------- /src/layouts/LevelBasicLayout.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed } from 'vue' 2 | import { RouterView, RouteRecordRaw, useRouter, useRoute } from 'vue-router' 3 | import { Layout, Dropdown, Menu } from 'ant-design-vue' 4 | import { GlobalHeader, Menus, LevelMenus } from '@/components/GlobalHeader' 5 | 6 | import styles from './index.module.less' 7 | import UserIcon from '../assets/user.png' 8 | 9 | const LevelBasicLayout = defineComponent({ 10 | name: 'LevelBasicLayout', 11 | setup() { 12 | const router = useRouter() 13 | const route = useRoute() 14 | 15 | // 获取显示状态的路由 16 | const menuLists = computed(() => { 17 | const menus = getMenus().filter((item: any) => !item?.meta?.hidden) 18 | menus.forEach((item: any) => { 19 | if (item.children) { 20 | // 只取第一层 21 | delete item.children 22 | } 23 | }) 24 | return menus 25 | }) 26 | 27 | // 获取子路由 28 | const subMenuLists = computed(() => { 29 | let menus: RouteRecordRaw[] = [] 30 | const names = route.path.split('/') 31 | if (names.length) { 32 | getMenus().forEach((item: RouteRecordRaw) => { 33 | if (item.name == names[1]) { 34 | menus = item?.children || [] 35 | } 36 | }) 37 | } 38 | return menus 39 | }) 40 | 41 | // 获取路由列表 42 | const getMenus = () => { 43 | let menuList: RouteRecordRaw[] = [] 44 | const routes: Array = router.options?.routes || [] 45 | routes.forEach((item) => { 46 | if (item.path == '/') { 47 | menuList = item.children || [] 48 | } 49 | }) 50 | return JSON.parse(JSON.stringify(menuList)) 51 | } 52 | 53 | // 退出 54 | const exit = () => { 55 | sessionStorage.clear() 56 | router.push('/login') 57 | } 58 | 59 | const menuSlots = { 60 | overlay: () => ( 61 | 62 | 63 | 退出 64 | 65 | 66 | ), 67 | } 68 | 69 | const slots = { 70 | content: () => ( 71 | <> 72 | {/* 导航栏 */} 73 | 74 | {/* 用户信息 */} 75 |
76 | 77 |
78 | 79 | admin 80 |
81 |
82 |
83 | 84 | ), 85 | } 86 | return () => ( 87 | 88 | 89 |
90 | {/* 侧边子目录 */} 91 | {subMenuLists['value'].length ? ( 92 |
93 | 94 |
95 | ) : null} 96 | {/* 右侧内容区 */} 97 | 98 | 99 | 100 |
101 |
102 | ) 103 | }, 104 | }) 105 | 106 | export default LevelBasicLayout 107 | -------------------------------------------------------------------------------- /src/layouts/RouteLayout.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, watch, ref } from 'vue' 2 | import { RouterView, useRoute } from 'vue-router' 3 | import ABreadCrumb from '@/components/ABreadCrumb' 4 | 5 | import styles from './index.module.less' 6 | 7 | const RouteLayout = defineComponent({ 8 | name: 'RouteLayout', 9 | props: { 10 | isSubView: { 11 | type: Boolean, 12 | default: true, 13 | }, 14 | }, 15 | setup(props, { slots }) { 16 | const route = useRoute() 17 | const routeName = ref(route.meta?.title) // 当前路由title 18 | 19 | watch(route, (val) => { 20 | routeName.value = val.meta?.title 21 | }) // 监听路由变化 22 | 23 | return () => ( 24 |
25 |
26 | 27 |
28 |
29 | {props.isSubView ? : slots.default?.()} 30 |
31 |
32 | ) 33 | }, 34 | }) 35 | 36 | export default RouteLayout 37 | -------------------------------------------------------------------------------- /src/layouts/SimplifyBasicLayout.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed } from 'vue' 2 | import { Layout, Dropdown, Menu } from 'ant-design-vue' 3 | import { GlobalHeader, Menus } from '@/components/GlobalHeader' 4 | import { RouterView, RouteRecordRaw, useRouter, useRoute } from 'vue-router' 5 | 6 | import styles from './index.module.less' 7 | import UserIcon from '../assets/user.png' 8 | 9 | const SimplifyBasicLayout = defineComponent({ 10 | name: 'SimplifyBasicLayout', 11 | setup() { 12 | const router = useRouter() 13 | 14 | // 获取路由列表 15 | const getMenus = () => { 16 | let menuList: RouteRecordRaw[] = [] 17 | const routes: Array = router.options?.routes || [] 18 | routes.forEach((item) => { 19 | if (item.path == '/') { 20 | menuList = item.children || [] 21 | } 22 | }) 23 | return JSON.parse(JSON.stringify(menuList)) 24 | } 25 | 26 | // 获取显示状态的路由 27 | const menuLists = computed(() => { 28 | return getMenus().filter((item: any) => !item?.meta?.hidden) 29 | }) 30 | 31 | // 退出 32 | const exit = () => { 33 | sessionStorage.clear() 34 | router.push('/login') 35 | } 36 | 37 | const menuSlots = { 38 | overlay: () => ( 39 | 40 | 41 | 退出 42 | 43 | 44 | ), 45 | } 46 | 47 | const slots = { 48 | content: () => ( 49 | <> 50 | {/* 用户信息 */} 51 |
52 | 53 |
54 | 55 | admin 56 |
57 |
58 |
59 | 60 | ), 61 | } 62 | return () => ( 63 | 64 | 65 |
66 | {/* 导航栏 */} 67 |
68 | 69 |
70 | 71 | 72 | 73 |
74 |
75 | ) 76 | }, 77 | }) 78 | 79 | export default SimplifyBasicLayout 80 | -------------------------------------------------------------------------------- /src/layouts/index.module.less: -------------------------------------------------------------------------------- 1 | .level-layout { 2 | padding-left: 0px; 3 | height: 100% !important; 4 | :global { 5 | .ant-layout-content { 6 | height: 100%; 7 | } 8 | } 9 | .level-content { 10 | display: flex; 11 | flex-flow: row nowrap; 12 | align-items: flex-start; 13 | height: 100%; 14 | .vertical-sub-menu { 15 | width: 210px; 16 | height: 100%; 17 | background: #fff; 18 | padding: 6px 0; 19 | } 20 | } 21 | .user-info { 22 | min-width: 100px; 23 | display: flex; 24 | flex-flow: row nowrap; 25 | align-items: center; 26 | justify-content: flex-end; 27 | .lang-box { 28 | width: 80px; 29 | margin-right: 10px; 30 | &.ant-select { 31 | font-size: 12px; 32 | } 33 | .ant-select-selection { 34 | background: transparent; 35 | border: 1px solid #a7a5a5; 36 | color: #fff; 37 | } 38 | .ant-select-arrow { 39 | color: #fff; 40 | font-size: 10px; 41 | } 42 | } 43 | .ant-dropdown-trigger { 44 | height: 54px; 45 | line-height: 54px; 46 | } 47 | .user-head { 48 | width: 30px; 49 | height: 30px; 50 | margin-right: 10px; 51 | margin-top: -4px; 52 | } 53 | .user-name { 54 | line-height: 54px; 55 | font-size: 16px; 56 | color: rgba(255, 255, 255, 0.85); 57 | } 58 | .icon { 59 | line-height: 54px; 60 | font-size: 24px; 61 | font-weight: bold; 62 | color: #fff; 63 | margin-left: 10px; 64 | cursor: pointer; 65 | } 66 | } 67 | } 68 | 69 | .page-view { 70 | height: 100%; 71 | display: flex; 72 | flex-flow: column nowrap; 73 | margin: 0 20px 10px 20px; 74 | padding-bottom: 10px; 75 | .top-info { 76 | background: #f0f2f5; 77 | padding: 10px 10px 10px 0; 78 | } 79 | .view-con { 80 | flex: 1; 81 | background: rgba(255, 255, 255, 0.9); 82 | border-radius: 4px; 83 | overflow-y: auto; 84 | overflow-x: hidden; 85 | padding: 10px; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | import LevelBasicLayout from './LevelBasicLayout' 2 | import SimplifyBasicLayout from './SimplifyBasicLayout' 3 | import RouteLayout from './RouteLayout' 4 | import BlankLayout from './BlankLayout' 5 | 6 | export { LevelBasicLayout, RouteLayout, BlankLayout, SimplifyBasicLayout } 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { store, key } from './store' 3 | import router from './router' 4 | import moment from 'moment' 5 | import App from './App' 6 | import Antd from 'ant-design-vue' 7 | 8 | moment.locale('zh-cn') 9 | 10 | import '@/mock' 11 | 12 | import './public/css/base.css' 13 | import './public/css/init.less' 14 | import '@/public/font/iconfont.css' 15 | import 'ant-design-vue/dist/antd.css' 16 | 17 | const app = createApp(App) 18 | 19 | app.use(router).use(store, key).use(Antd).mount('#app') 20 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | import '@/mock/user/login'; 3 | import '@/mock/user/permission'; 4 | import '@/mock/user/logout'; 5 | 6 | // 设置全局延时 7 | Mock.setup({ 8 | timeout: '300-600' 9 | }); 10 | -------------------------------------------------------------------------------- /src/mock/result.ts: -------------------------------------------------------------------------------- 1 | export class Result { 2 | public code?: number; 3 | 4 | public data?: any; 5 | 6 | public status?: boolean; 7 | 8 | public msg?: string 9 | 10 | constructor(code = 200, data?: any, status = true, msg?: string) { 11 | this.code = code; 12 | this.data = data; 13 | this.status = status; 14 | this.msg = msg; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/mock/user/login.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { uuid } from '@/utils/util' 3 | import { Result } from '../result'; 4 | 5 | const BaseUrl = import.meta.env.VITE_API_BASE_URL as string 6 | 7 | const user = Mock.mock({ 8 | username: 'admin', 9 | address: '成都市高新区天府四街' 10 | }); 11 | 12 | Mock.mock(`${BaseUrl}/login`, 'post', ({ body }: { body: string }) => { 13 | const result = new Result(); 14 | const { username, password } = JSON.parse(body); 15 | 16 | if (username !== 'admin' || password !== '666666') { 17 | result.status = false; 18 | result.msg = '账户名或密码错误(admin/666666)'; 19 | } else { 20 | result.data = { 21 | userInfo: user, 22 | token: uuid() 23 | }; 24 | } 25 | return result; 26 | }); 27 | -------------------------------------------------------------------------------- /src/mock/user/logout.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { Result } from '../result'; 3 | 4 | const BaseUrl = import.meta.env.VITE_API_BASE_URL as string 5 | 6 | Mock.mock(`${BaseUrl}/logout`, 'get', () => { 7 | const result = new Result(); 8 | result.msg = '退出成功!'; 9 | return result; 10 | }); 11 | -------------------------------------------------------------------------------- /src/mock/user/permission.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { Result } from '../result' 3 | 4 | const BaseUrl = import.meta.env.VITE_API_BASE_URL as string 5 | 6 | const Roles = [ 7 | { 8 | url: '/dataProtal', // 模块路径&访问路由 9 | name: '/dataProtal', 10 | title: '数据门户', 11 | icon: 'icon-index-copy', 12 | }, 13 | { 14 | url: '/dataManage', 15 | name: '/dataManage', 16 | title: '数据管理', 17 | icon: 'icon-shuju', 18 | }, 19 | { 20 | url: '/designCenter', 21 | name: '/designCenter', 22 | title: '设计中心', 23 | icon: 'icon-shejishi2', 24 | list: [ 25 | { 26 | url: '/designCenter/screenManage', 27 | name: 'screenManage', 28 | title: '画面管理', 29 | icon: 'icon-shituzhushitu', 30 | }, 31 | { 32 | url: '/designCenter/materialMange', 33 | name: 'materialMange', 34 | title: '素材中心', 35 | icon: 'icon-sucai', 36 | list: [ 37 | { 38 | url: '/designCenter/materialMange/customControl', 39 | name: 'customControl', 40 | title: '自定义控件', 41 | icon: 'icon-zidingyi', 42 | }, 43 | { 44 | url: '/designCenter/materialMange/customMaterial', 45 | name: 'customMaterial', 46 | title: '自定义素材', 47 | icon: 'icon-jichukongjiantubiao-gonggongxuanzekuang', 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | { 54 | url: '/userManage', 55 | name: '/userManage', 56 | title: '用户管理', 57 | icon: 'icon-yonghuguanli', 58 | }, 59 | ] 60 | 61 | Mock.mock(`${BaseUrl}/navPerson`, 'get', () => { 62 | const result = new Result() 63 | const menuList: StoreState.Role[] = Roles 64 | result.data = { 65 | menuList, 66 | permissions: [], 67 | } 68 | return result 69 | }) 70 | -------------------------------------------------------------------------------- /src/plugins/antd.ts: -------------------------------------------------------------------------------- 1 | import { Button, Card, message, Breadcrumb } from 'ant-design-vue'; 2 | 3 | const plugins = [Button, Card, Breadcrumb]; 4 | 5 | export const setupAntd = (app: any, options = {}) => { 6 | app.config.globalProperties.$message = message; 7 | plugins.forEach((plugin) => { 8 | app.use(plugin); 9 | }); 10 | }; -------------------------------------------------------------------------------- /src/public/css/base.css: -------------------------------------------------------------------------------- 1 | /*css 初始化 */ 2 | html, 3 | body, 4 | ul, 5 | li, 6 | ol, 7 | dl, 8 | dd, 9 | dt, 10 | p, 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | h5, 16 | h6, 17 | form, 18 | fieldset, 19 | legend, 20 | img { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | html, 26 | body, 27 | #app, 28 | .ivu-layout { 29 | width: 100%; 30 | height: 100%; 31 | } 32 | 33 | fieldset, 34 | img, 35 | input, 36 | button { 37 | border: none; 38 | padding: 0; 39 | margin: 0; 40 | outline-style: none; 41 | font: 14px/1.5 'Microsoft YaHei', Arial, Tahoma, Helvetica, sans-serif; 42 | font-family: sans-serif; 43 | } 44 | 45 | /*去掉input等聚焦时的蓝色边框*/ 46 | ul, 47 | ol { 48 | list-style: none; 49 | } 50 | 51 | input { 52 | padding-top: 0; 53 | padding-bottom: 0; 54 | } 55 | 56 | select, 57 | input { 58 | vertical-align: middle; 59 | } 60 | 61 | select, 62 | input, 63 | textarea { 64 | font-size: 12px; 65 | margin: 0; 66 | } 67 | 68 | textarea { 69 | resize: none; 70 | font-family: sans-serif; 71 | } 72 | 73 | /*防止拖动*/ 74 | img { 75 | border: 0; 76 | vertical-align: middle; 77 | } 78 | 79 | /* 去掉图片低测默认的3像素空白缝隙,或者用display:block也可以*/ 80 | table { 81 | border-collapse: collapse; 82 | } 83 | 84 | body { 85 | font: 12px Arial, Verdana, '\5b8b\4f53'; 86 | color: #666; 87 | font-family: '微软雅黑'; 88 | -webkit-box-sizing: border-box; 89 | -moz-box-sizing: border-box; 90 | box-sizing: border-box; 91 | } 92 | 93 | .clearfix:before, 94 | .clearfix:after { 95 | /*清楚浮动*/ 96 | content: ''; 97 | display: table; 98 | } 99 | 100 | .clearfix:after { 101 | clear: both; 102 | } 103 | 104 | .clearfix { 105 | *zoom: 1; /*IE/7/6*/ 106 | } 107 | 108 | a { 109 | color: #666; 110 | text-decoration: none; 111 | } 112 | 113 | a:hover { 114 | color: #279ee4; 115 | } 116 | 117 | a:focus { 118 | text-decoration: none; 119 | } 120 | 121 | h1, 122 | h2, 123 | h3, 124 | h4, 125 | h5, 126 | h6 { 127 | text-decoration: none; 128 | font-weight: normal; 129 | font-size: 100%; 130 | } 131 | 132 | /*设置h标签的大小,设置跟父亲一样大的字体font-size:100%;*/ 133 | s, 134 | i, 135 | em { 136 | font-style: normal; 137 | text-decoration: none; 138 | } 139 | 140 | .col-red { 141 | color: #c81623 !important; 142 | } 143 | 144 | /*公共类*/ 145 | .w { 146 | /*版心 提取 */ 147 | width: 1210px; 148 | margin: 0 auto; 149 | } 150 | 151 | .fl { 152 | float: left; 153 | } 154 | 155 | .fr { 156 | float: right; 157 | } 158 | 159 | .al { 160 | text-align: left; 161 | } 162 | 163 | .ac { 164 | text-align: center; 165 | } 166 | 167 | .ar { 168 | text-align: right; 169 | } 170 | 171 | .hide { 172 | display: none; 173 | } 174 | 175 | .font12 { 176 | font-size: 12px; 177 | } 178 | 179 | .font14 { 180 | font-size: 14px; 181 | } 182 | 183 | .font16 { 184 | font-size: 16px; 185 | } 186 | -------------------------------------------------------------------------------- /src/public/css/init.less: -------------------------------------------------------------------------------- 1 | //自定义滚动条 2 | ::-webkit-scrollbar { 3 | /*滚动条整体样式*/ 4 | width: 6px; 5 | /*高宽分别对应横竖滚动条的尺寸*/ 6 | height: 6px; 7 | } 8 | 9 | ::-webkit-scrollbar-thumb { 10 | /*滚动条里面小方块*/ 11 | border-radius: 10px; 12 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); 13 | background: #b4b4b4; 14 | } 15 | 16 | ::-webkit-scrollbar-track { 17 | /*滚动条里面轨道*/ 18 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); 19 | border-radius: 10px; 20 | background: #ededed; 21 | } 22 | 23 | ::-webkit-scrollbar-thumb:hover { 24 | background-color: rgba(#101f1c, 0.7); 25 | } 26 | 27 | // 自定义table样式 28 | .my-table { 29 | .ant-table-thead > tr > th, 30 | .ant-table-tbody > tr > td { 31 | padding: 10px 10px; 32 | background: #181a1d; 33 | border-bottom: 0px solid #3e3e3e; 34 | color: #fff; 35 | } 36 | 37 | .ant-table-tbody > tr > td { 38 | padding: 6px 10px; 39 | } 40 | 41 | .ant-table-thead > tr:hover.ant-table-row-selected > td, 42 | .ant-table-tbody > tr:hover.ant-table-row-selected > td { 43 | background: rgba(0, 0, 0, 0.6); 44 | color: #fff; 45 | } 46 | 47 | .ant-table-tbody > tr.ant-table-row-selected td { 48 | background: rgba(0, 0, 0, 0.6); 49 | color: #fff; 50 | } 51 | 52 | .ant-table-thead > tr:first-child > th:last-child, 53 | .ant-table-thead > tr:first-child > th:first-child { 54 | border-radius: 0; 55 | } 56 | 57 | .ant-form-item label { 58 | color: rgba(255, 255, 255, 0.8); 59 | } 60 | 61 | .ant-table-fixed-header 62 | > .ant-table-content 63 | > .ant-table-scroll 64 | > .ant-table-body { 65 | background: #181a1d; 66 | } 67 | 68 | .ant-table-thead 69 | > tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) 70 | > td, 71 | .ant-table-tbody 72 | > tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) 73 | > td, 74 | .ant-table-thead 75 | > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) 76 | > td, 77 | .ant-table-tbody 78 | > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) 79 | > td { 80 | background: rgba(0, 0, 0, 0.4); 81 | } 82 | 83 | .ant-table-fixed-header .ant-table-scroll .ant-table-header { 84 | margin: 0 !important; 85 | overflow: hidden !important; 86 | } 87 | 88 | .ant-table-placeholder { 89 | color: #fff; 90 | background: #272727; 91 | border-top: 1px solid #3e3e3e; 92 | border-bottom: 1px solid #3e3e3e; 93 | } 94 | 95 | .ant-empty-normal { 96 | color: #fff; 97 | } 98 | } 99 | 100 | //自定义标签页样式 101 | .my-tabs { 102 | .ant-tabs-bar { 103 | border-bottom: 1px solid #3e3e3e; 104 | } 105 | 106 | .ant-tabs-nav-container { 107 | margin-bottom: -5px; 108 | } 109 | 110 | .ant-tabs-card-bar .ant-tabs-tab { 111 | height: 36px !important; 112 | margin: 0; 113 | padding: 0 16px; 114 | line-height: 34px; 115 | color: #fff; 116 | background: rgba(0, 0, 0, 0) !important; 117 | border: 1px solid #3e3e3e !important; 118 | border-radius: 4px 4px 0 0; 119 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 120 | } 121 | 122 | .ant-tabs-card-bar .ant-tabs-tab-active { 123 | color: #fff !important; 124 | background: #0972fe !important; 125 | } 126 | } 127 | 128 | //自定义model样式 129 | .my-modal { 130 | .ant-modal { 131 | .ant-modal-content { 132 | padding: 10px 10px; 133 | background: #000; 134 | color: #fff; 135 | 136 | .ant-modal-close { 137 | width: 47px; 138 | height: 47px; 139 | line-height: 39px; 140 | 141 | .ant-modal-close-x { 142 | font-size: 15px; 143 | color: #fff; 144 | } 145 | } 146 | 147 | .ant-modal-confirm-title { 148 | color: #fff; 149 | } 150 | 151 | .ant-modal-confirm-content { 152 | color: rgba(255, 255, 255, 0.8); 153 | } 154 | } 155 | 156 | .ant-modal-header { 157 | background: rgba(0, 0, 0, 0); 158 | border-bottom: 1px solid hsla(0, 0%, 94.9%, 0.2); 159 | border-radius: 4px 4px 0 0; 160 | padding: 8px 16px !important; 161 | 162 | .ant-modal-title { 163 | color: #fff; 164 | font-size: 15px; 165 | } 166 | } 167 | } 168 | } 169 | 170 | //自定义form样式 171 | .my-form { 172 | .ant-form-item-label > label { 173 | color: #fff !important; 174 | font-size: 14px !important; 175 | } 176 | 177 | .ant-form-item-control textarea .ant-input, 178 | .ant-input, 179 | .ant-input-affix-wrapper .ant-input, 180 | .ant-select .ant-select-selection { 181 | background: rgba(51, 55, 59, 0.1); 182 | border: 1px solid rgba(51, 55, 59, 0.9); 183 | color: #fff; 184 | } 185 | 186 | .has-error .ant-input, 187 | .has-error .ant-input:hover { 188 | background-color: transparent; 189 | } 190 | 191 | .ant-input[disabled] { 192 | color: #fff; 193 | background-color: #303030; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/public/font/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /src/public/font/demo_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | iconfont Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 36 | 37 | 38 |
39 |

40 | 41 | 42 |

43 | 53 |
54 |
55 |
    56 | 57 |
  • 58 | 59 |
    设计师2
    60 |
    &#xe643;
    61 |
  • 62 | 63 |
  • 64 | 65 |
    基础控件图标-公共选择框
    66 |
    &#xedf9;
    67 |
  • 68 | 69 |
  • 70 | 71 |
    自定义
    72 |
    &#xe723;
    73 |
  • 74 | 75 |
  • 76 | 77 |
    素材
    78 |
    &#xe833;
    79 |
  • 80 | 81 |
  • 82 | 83 |
    用户管理
    84 |
    &#xe629;
    85 |
  • 86 | 87 |
  • 88 | 89 |
    【视图】主视图
    90 |
    &#xe650;
    91 |
  • 92 | 93 |
  • 94 | 95 |
    首页-选中
    96 |
    &#xe622;
    97 |
  • 98 | 99 |
  • 100 | 101 |
    数据
    102 |
    &#xe73c;
    103 |
  • 104 | 105 |
106 |
107 |

Unicode 引用

108 |
109 | 110 |

Unicode 是字体在网页端最原始的应用方式,特点是:

111 |
    112 |
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 113 |
  • 默认情况下不支持多色,直接添加多色图标会自动去色。
  • 114 |
115 |
116 |

注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

117 |
118 |

Unicode 使用步骤如下:

119 |

第一步:拷贝项目下面生成的 @font-face

120 |
@font-face {
122 |   font-family: 'iconfont';
123 |   src: url('iconfont.woff2?t=1631943552541') format('woff2'),
124 |        url('iconfont.woff?t=1631943552541') format('woff'),
125 |        url('iconfont.ttf?t=1631943552541') format('truetype');
126 | }
127 | 
128 |

第二步:定义使用 iconfont 的样式

129 |
.iconfont {
131 |   font-family: "iconfont" !important;
132 |   font-size: 16px;
133 |   font-style: normal;
134 |   -webkit-font-smoothing: antialiased;
135 |   -moz-osx-font-smoothing: grayscale;
136 | }
137 | 
138 |

第三步:挑选相应图标并获取字体编码,应用于页面

139 |
140 | <span class="iconfont">&#x33;</span>
142 | 
143 |
144 |

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

145 |
146 |
147 |
148 |
149 |
    150 | 151 |
  • 152 | 153 |
    154 | 设计师2 155 |
    156 |
    .icon-shejishi2 157 |
    158 |
  • 159 | 160 |
  • 161 | 162 |
    163 | 基础控件图标-公共选择框 164 |
    165 |
    .icon-jichukongjiantubiao-gonggongxuanzekuang 166 |
    167 |
  • 168 | 169 |
  • 170 | 171 |
    172 | 自定义 173 |
    174 |
    .icon-zidingyi 175 |
    176 |
  • 177 | 178 |
  • 179 | 180 |
    181 | 素材 182 |
    183 |
    .icon-sucai 184 |
    185 |
  • 186 | 187 |
  • 188 | 189 |
    190 | 用户管理 191 |
    192 |
    .icon-yonghuguanli 193 |
    194 |
  • 195 | 196 |
  • 197 | 198 |
    199 | 【视图】主视图 200 |
    201 |
    .icon-shituzhushitu 202 |
    203 |
  • 204 | 205 |
  • 206 | 207 |
    208 | 首页-选中 209 |
    210 |
    .icon-index-copy 211 |
    212 |
  • 213 | 214 |
  • 215 | 216 |
    217 | 数据 218 |
    219 |
    .icon-shuju 220 |
    221 |
  • 222 | 223 |
224 |
225 |

font-class 引用

226 |
227 | 228 |

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

229 |

与 Unicode 使用方式相比,具有如下特点:

230 |
    231 |
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • 232 |
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • 233 |
234 |

使用步骤如下:

235 |

第一步:引入项目下面生成的 fontclass 代码:

236 |
<link rel="stylesheet" href="./iconfont.css">
237 | 
238 |

第二步:挑选相应图标并获取类名,应用于页面:

239 |
<span class="iconfont icon-xxx"></span>
240 | 
241 |
242 |

" 243 | iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

244 |
245 |
246 |
247 |
248 |
    249 | 250 |
  • 251 | 254 |
    设计师2
    255 |
    #icon-shejishi2
    256 |
  • 257 | 258 |
  • 259 | 262 |
    基础控件图标-公共选择框
    263 |
    #icon-jichukongjiantubiao-gonggongxuanzekuang
    264 |
  • 265 | 266 |
  • 267 | 270 |
    自定义
    271 |
    #icon-zidingyi
    272 |
  • 273 | 274 |
  • 275 | 278 |
    素材
    279 |
    #icon-sucai
    280 |
  • 281 | 282 |
  • 283 | 286 |
    用户管理
    287 |
    #icon-yonghuguanli
    288 |
  • 289 | 290 |
  • 291 | 294 |
    【视图】主视图
    295 |
    #icon-shituzhushitu
    296 |
  • 297 | 298 |
  • 299 | 302 |
    首页-选中
    303 |
    #icon-index-copy
    304 |
  • 305 | 306 |
  • 307 | 310 |
    数据
    311 |
    #icon-shuju
    312 |
  • 313 | 314 |
315 |
316 |

Symbol 引用

317 |
318 | 319 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 320 | 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

321 |
    322 |
  • 支持多色图标了,不再受单色限制。
  • 323 |
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • 324 |
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • 325 |
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • 326 |
327 |

使用步骤如下:

328 |

第一步:引入项目下面生成的 symbol 代码:

329 |
<script src="./iconfont.js"></script>
330 | 
331 |

第二步:加入通用 CSS 代码(引入一次就行):

332 |
<style>
333 | .icon {
334 |   width: 1em;
335 |   height: 1em;
336 |   vertical-align: -0.15em;
337 |   fill: currentColor;
338 |   overflow: hidden;
339 | }
340 | </style>
341 | 
342 |

第三步:挑选相应图标并获取类名,应用于页面:

343 |
<svg class="icon" aria-hidden="true">
344 |   <use xlink:href="#icon-xxx"></use>
345 | </svg>
346 | 
347 |
348 |
349 | 350 |
351 |
352 | 371 | 372 | 373 | -------------------------------------------------------------------------------- /src/public/font/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2822175 */ 3 | src: url('iconfont.woff2?t=1631943552541') format('woff2'), 4 | url('iconfont.woff?t=1631943552541') format('woff'), 5 | url('iconfont.ttf?t=1631943552541') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-shejishi2:before { 17 | content: "\e643"; 18 | } 19 | 20 | .icon-jichukongjiantubiao-gonggongxuanzekuang:before { 21 | content: "\edf9"; 22 | } 23 | 24 | .icon-zidingyi:before { 25 | content: "\e723"; 26 | } 27 | 28 | .icon-sucai:before { 29 | content: "\e833"; 30 | } 31 | 32 | .icon-yonghuguanli:before { 33 | content: "\e629"; 34 | } 35 | 36 | .icon-shituzhushitu:before { 37 | content: "\e650"; 38 | } 39 | 40 | .icon-index-copy:before { 41 | content: "\e622"; 42 | } 43 | 44 | .icon-shuju:before { 45 | content: "\e73c"; 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/public/font/iconfont.js: -------------------------------------------------------------------------------- 1 | !function(t){var c,e,o,i,l,n='',h=(h=document.getElementsByTagName("script"))[h.length-1].getAttribute("data-injectcss"),a=function(t,c){c.parentNode.insertBefore(t,c)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}function d(){l||(l=!0,o())}function s(){try{i.documentElement.doScroll("left")}catch(t){return void setTimeout(s,50)}d()}c=function(){var t,c;(c=document.createElement("div")).innerHTML=n,n=null,(t=c.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",c=t,(t=document.body).firstChild?a(c,t.firstChild):t.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(e=function(){document.removeEventListener("DOMContentLoaded",e,!1),c()},document.addEventListener("DOMContentLoaded",e,!1)):document.attachEvent&&(o=c,i=t.document,l=!1,s(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,d())})}(window); -------------------------------------------------------------------------------- /src/public/font/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2822175", 3 | "name": "my-project", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "714762", 10 | "name": "设计师2", 11 | "font_class": "shejishi2", 12 | "unicode": "e643", 13 | "unicode_decimal": 58947 14 | }, 15 | { 16 | "icon_id": "8059416", 17 | "name": "基础控件图标-公共选择框", 18 | "font_class": "jichukongjiantubiao-gonggongxuanzekuang", 19 | "unicode": "edf9", 20 | "unicode_decimal": 60921 21 | }, 22 | { 23 | "icon_id": "13795610", 24 | "name": "自定义", 25 | "font_class": "zidingyi", 26 | "unicode": "e723", 27 | "unicode_decimal": 59171 28 | }, 29 | { 30 | "icon_id": "13901301", 31 | "name": "素材", 32 | "font_class": "sucai", 33 | "unicode": "e833", 34 | "unicode_decimal": 59443 35 | }, 36 | { 37 | "icon_id": "15633946", 38 | "name": "用户管理", 39 | "font_class": "yonghuguanli", 40 | "unicode": "e629", 41 | "unicode_decimal": 58921 42 | }, 43 | { 44 | "icon_id": "15773666", 45 | "name": "【视图】主视图", 46 | "font_class": "shituzhushitu", 47 | "unicode": "e650", 48 | "unicode_decimal": 58960 49 | }, 50 | { 51 | "icon_id": "19985212", 52 | "name": "首页-选中", 53 | "font_class": "index-copy", 54 | "unicode": "e622", 55 | "unicode_decimal": 58914 56 | }, 57 | { 58 | "icon_id": "22770862", 59 | "name": "数据", 60 | "font_class": "shuju", 61 | "unicode": "e73c", 62 | "unicode_decimal": 59196 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/public/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/public/font/iconfont.ttf -------------------------------------------------------------------------------- /src/public/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/public/font/iconfont.woff -------------------------------------------------------------------------------- /src/public/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/public/font/iconfont.woff2 -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | createWebHashHistory, 4 | createWebHistory, 5 | RouteRecordRaw, 6 | RouteLocationNormalized, 7 | NavigationGuardNext, 8 | Router, 9 | } from 'vue-router' 10 | import { mainRoutes, baseRoutes, Routes } from './router.config' 11 | import { getToken } from '@/utils/token' 12 | import UserApi from '@/api/user' 13 | import { RouteLayout, BlankLayout } from '@/layouts' 14 | 15 | let isAddDynamicMenuRoutes = false // 是否请求路由表 16 | 17 | // 路由实例 18 | const router: Router = createRouter({ 19 | history: createWebHistory(), 20 | routes: baseRoutes, 21 | }) 22 | 23 | // 构造路由表 24 | function fnAddDynamicMenuRoutes( 25 | menuList: StoreState.Role[], 26 | routes: RouteRecordRaw[], 27 | ) { 28 | menuList.forEach((item) => { 29 | if (!item.list) { 30 | routes.push({ 31 | path: `${item.url}`, 32 | name: item.name.slice(0, 1) == '/' ? item.name.slice(1) : item.name, // 截取开头"/" 33 | component: () => import(`../views${item.url}/index`), 34 | meta: { 35 | title: item.title, 36 | hidden: false, 37 | icon: item.icon, 38 | }, 39 | }) 40 | } else if (item.list && item.list.length) { 41 | const menus = fnAddDynamicMenuRoutes(item.list, []) 42 | const paths = item.url.split('/') 43 | routes.push({ 44 | path: `${item.url}`, 45 | name: item.name.slice(0, 1) == '/' ? item.name.slice(1) : item.name, // 截取开头"/" 46 | component: paths.length > 2 ? BlankLayout : RouteLayout, // 二级路由 47 | redirect: menus[0].path, 48 | children: menus, 49 | meta: { 50 | title: item.title, 51 | hidden: false, 52 | icon: item.icon, 53 | }, 54 | }) 55 | } 56 | }) 57 | return routes 58 | } 59 | 60 | router.beforeEach( 61 | async ( 62 | to: RouteLocationNormalized, 63 | from: RouteLocationNormalized, 64 | next: NavigationGuardNext, 65 | ) => { 66 | const token: string = getToken() as string 67 | if (token) { 68 | // 第一次加载路由列表并且该项目需要动态路由 69 | if (!isAddDynamicMenuRoutes) { 70 | try { 71 | //获取动态路由表 72 | const res: any = await UserApi.getPermissionsList({}) 73 | if (res.code == 200) { 74 | isAddDynamicMenuRoutes = true 75 | const menu = res.data 76 | // 通过路由表构造路由 77 | const menuRoutes: any = fnAddDynamicMenuRoutes( 78 | menu.menuList || [], 79 | [], 80 | ) 81 | mainRoutes.children = [] 82 | mainRoutes.children?.unshift(...menuRoutes, ...Routes) 83 | // 动态添加路由 84 | router.addRoute(mainRoutes) 85 | // 注:这步很关键,不然导航获取不到路由 86 | router.options.routes.unshift(mainRoutes) 87 | // 本地存储按钮权限集合 88 | sessionStorage.setItem( 89 | 'permissions', 90 | JSON.stringify(menu.permissions || '[]'), 91 | ) 92 | if (to.path == '/' || to.path == '/login') { 93 | const firstName = menuRoutes.length && menuRoutes[0].name 94 | next({ name: firstName, replace: true }) 95 | } else { 96 | next({ path: to.fullPath }) 97 | } 98 | } else { 99 | sessionStorage.setItem('menuList', '[]') 100 | sessionStorage.setItem('permissions', '[]') 101 | next() 102 | } 103 | } catch (error) { 104 | console.log( 105 | `%c${error} 请求菜单列表和权限失败,跳转至登录页!!`, 106 | 'color:orange', 107 | ) 108 | } 109 | } else { 110 | if (to.path == '/' || to.path == '/login') { 111 | next(from) 112 | } else { 113 | next() 114 | } 115 | } 116 | } else { 117 | isAddDynamicMenuRoutes = false 118 | if (to.name != 'login') { 119 | next({ name: 'login' }) 120 | } 121 | next() 122 | } 123 | }, 124 | ) 125 | 126 | router.onError((handler) => { 127 | console.log(handler) 128 | }) 129 | 130 | export default router 131 | -------------------------------------------------------------------------------- /src/router/router.config.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import { LevelBasicLayout, RouteLayout } from '@/layouts' 3 | 4 | // 导航路由 5 | const Routes: Array = [ 6 | { 7 | path: '/403', 8 | name: '403', 9 | component: () => 10 | import(/* webpackChunkName: "403" */ '@/views/exception/403'), 11 | meta: { title: '403', permission: ['exception'], hidden: true }, 12 | }, 13 | { 14 | path: '/404', 15 | name: '404', 16 | component: () => 17 | import(/* webpackChunkName: "404" */ '@/views/exception/404'), 18 | meta: { title: '404', permission: ['exception'], hidden: true }, 19 | }, 20 | { 21 | path: '/500', 22 | name: '500', 23 | component: () => 24 | import(/* webpackChunkName: "500" */ '@/views/exception/500'), 25 | meta: { title: '500', permission: ['exception'], hidden: true }, 26 | }, 27 | { 28 | path: '/:pathMatch(.*)*', 29 | name: 'error', 30 | component: () => 31 | import(/* webpackChunkName: "500" */ '@/views/exception/404'), 32 | meta: { title: '404', hidden: true }, 33 | }, 34 | ] 35 | 36 | // 主路由 37 | const mainRoutes: RouteRecordRaw = { 38 | path: '/', 39 | redirect: '/login', 40 | component: LevelBasicLayout, 41 | children: [], 42 | } 43 | 44 | // 基础路由 45 | const baseRoutes: Array = [ 46 | { 47 | path: '/login', 48 | name: 'login', 49 | component: () => 50 | import(/* webpackChunkName: "login" */ '@/views/user/login'), 51 | }, 52 | ] 53 | 54 | export { mainRoutes, baseRoutes, Routes } 55 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from 'vue' 2 | import { createStore, Store } from 'vuex' 3 | import common from './modules/common' 4 | 5 | export interface State {} 6 | 7 | export const key: InjectionKey> = Symbol() 8 | 9 | export const store = createStore({ 10 | modules: { common }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/store/modules/common.ts: -------------------------------------------------------------------------------- 1 | interface State { 2 | count: number 3 | } 4 | 5 | const initPageState = () => { 6 | return { 7 | count: 1, 8 | } 9 | } 10 | 11 | const common = { 12 | state: initPageState(), 13 | getters: { 14 | getCount: (state: State) => state.count, 15 | }, 16 | mutations: { 17 | setCountNum(state: State) { 18 | state.count++ 19 | }, 20 | }, 21 | } 22 | 23 | export default common 24 | -------------------------------------------------------------------------------- /src/types/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 3 | * @Author: chenxin 4 | * @Date: 2020-09-22 16:11:50 5 | * @LastEditors: Please set LastEditors 6 | * @LastEditTime: 2020-11-27 10:09:24 7 | */ 8 | declare namespace StoreState { 9 | // 接口返回结构 10 | export interface ResType { 11 | code: number 12 | data: T 13 | msg?: string 14 | status: boolean 15 | } 16 | 17 | // 请求参数结构 18 | export interface FetchParams { 19 | url: string 20 | data: object | string 21 | [propName: string]: any 22 | } 23 | 24 | // 权限返回实体 25 | export interface Role { 26 | icon?: string 27 | list?: Role[] 28 | menuId?: number 29 | name: string 30 | perms?: string 31 | requestPath?: string 32 | url: string 33 | [propName: string]: any 34 | } 35 | 36 | // layout布局 37 | export interface Layout { 38 | labelCol: { 39 | span: number 40 | } 41 | wrapperCol: { 42 | span: number 43 | } 44 | } 45 | 46 | // 分页参数 47 | export interface Pagination { 48 | current?: number 49 | position?: string 50 | pageSize?: number 51 | total?: number 52 | showQuickJumper?: boolean 53 | showTotal?: (total: number, range: [number, number]) => any 54 | } 55 | 56 | // 上传文件 57 | export interface FileItem { 58 | uid: string 59 | name?: string 60 | status?: string 61 | response?: string 62 | url?: string 63 | type?: string 64 | size: number 65 | originFileObj: any 66 | } 67 | 68 | export interface FileInfo { 69 | file: FileItem 70 | fileList: FileItem[] 71 | } 72 | 73 | // 资源类型 74 | export interface ResourceTypes { 75 | '1': string 76 | '2': string 77 | '3': string 78 | [key: string]: string 79 | } 80 | 81 | // 列表mixin 82 | export interface TableMixinOptions { 83 | queryTableApi: ({}) => any 84 | deleteApi: ({}) => any 85 | } 86 | 87 | // 列表查询参数 88 | export interface TableQueryOptions { 89 | orgId?: string 90 | pageNo: number 91 | pageSize: number 92 | [propName: string]: any 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | 7 | declare module '@*' 8 | declare module '@/utils/*' 9 | declare module 'crypto-js' 10 | declare module 'spark-md5' 11 | declare module 'qs' 12 | declare module 'mockjs' 13 | -------------------------------------------------------------------------------- /src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/utils/const.ts: -------------------------------------------------------------------------------- 1 | // 定义分页类 2 | export class Pagination { 3 | private Page: StoreState.Pagination = { 4 | position: 'bottom', 5 | showQuickJumper: true, 6 | current: 1, 7 | pageSize: 10, 8 | total: 0, 9 | showTotal: (total: number) => `总 ${total} 条`, 10 | } 11 | 12 | constructor(params?: StoreState.Pagination) { 13 | Object.assign(this.Page, params) 14 | } 15 | 16 | init() { 17 | return this.Page 18 | } 19 | } 20 | 21 | // modal公共属性 22 | export const modalProps = { 23 | maskClosable: false, 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios' 2 | import { getToken } from './token' 3 | import { Modal } from 'ant-design-vue' 4 | import { Message, Notification } from '@/utils/resetMessage' 5 | 6 | // .env环境变量 7 | const BaseUrl = import.meta.env.VITE_API_BASE_URL as string 8 | 9 | // create an axios instance 10 | const service: AxiosInstance = axios.create({ 11 | baseURL: BaseUrl, // 正式环境 12 | timeout: 60 * 1000, 13 | headers: {} 14 | }) 15 | 16 | /** 17 | * 请求拦截 18 | */ 19 | service.interceptors.request.use( 20 | (config: AxiosRequestConfig) => { 21 | config.headers.common.Authorization = getToken() // 请求头带上token 22 | config.headers.common.token = getToken() 23 | return config 24 | }, 25 | error => Promise.reject(error) 26 | ) 27 | 28 | /** 29 | * 响应拦截 30 | */ 31 | service.interceptors.response.use( 32 | (response: AxiosResponse) => { 33 | if (response.status == 201 || response.status == 200) { 34 | const { code, status, msg } = response.data 35 | if (code == 401) { 36 | Modal.warning({ 37 | title: 'token出错', 38 | content: 'token失效,请重新登录!', 39 | onOk: () => { 40 | sessionStorage.clear() 41 | } 42 | }) 43 | } else if (code == 200) { 44 | if (status) { 45 | // 接口请求成功 46 | msg && Message.success(msg) // 后台如果返回了msg,则将msg提示出来 47 | return Promise.resolve(response) // 返回成功数据 48 | } 49 | // 接口异常 50 | msg && Message.warning(msg) // 后台如果返回了msg,则将msg提示出来 51 | return Promise.reject(response) // 返回异常数据 52 | } else { 53 | // 接口异常 54 | msg && Message.error(msg) 55 | return Promise.reject(response) 56 | } 57 | } 58 | return response 59 | }, 60 | error => { 61 | if (error.response.status) { 62 | switch (error.response.status) { 63 | case 500: 64 | Notification.error({ 65 | message: '温馨提示', 66 | description: '服务异常,请重启服务器!' 67 | }) 68 | break 69 | case 401: 70 | Notification.error({ 71 | message: '温馨提示', 72 | description: '服务异常,请重启服务器!' 73 | }) 74 | break 75 | case 403: 76 | Notification.error({ 77 | message: '温馨提示', 78 | description: '服务异常,请重启服务器!' 79 | }) 80 | break 81 | // 404请求不存在 82 | case 404: 83 | Notification.error({ 84 | message: '温馨提示', 85 | description: '服务异常,请重启服务器!' 86 | }) 87 | break 88 | default: 89 | Notification.error({ 90 | message: '温馨提示', 91 | description: '服务异常,请重启服务器!' 92 | }) 93 | } 94 | } 95 | return Promise.reject(error.response) 96 | } 97 | ) 98 | 99 | interface Http { 100 | fetch(params: AxiosRequestConfig): Promise> 101 | } 102 | 103 | const http: Http = { 104 | fetch(params) { 105 | return new Promise((resolve, reject) => { 106 | service(params) 107 | .then(res => { 108 | resolve(res.data) 109 | }) 110 | .catch(err => { 111 | reject(err.data) 112 | }) 113 | }) 114 | } 115 | } 116 | 117 | export default http['fetch'] 118 | -------------------------------------------------------------------------------- /src/utils/resetMessage.js: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue' 2 | import notification from 'ant-design-vue/es/notification' 3 | 4 | const Message = (options) => { 5 | message.destroy() 6 | message[options.type](options) 7 | } 8 | 9 | const Notification = (options) => { 10 | notification.destroy() 11 | notification[options.type](options) 12 | }; 13 | 14 | ['success', 'info', 'warning', 'error', 'loading'].forEach((type) => { 15 | Message[type] = (options) => { 16 | if (typeof options === 'string') { 17 | options = { 18 | content: options, 19 | } 20 | } 21 | options.type = type 22 | return Message(options) 23 | } 24 | }); 25 | 26 | ['success', 'info', 'warning', 'error', 'loading', 'warn', 'open'].forEach( 27 | (type) => { 28 | Notification[type] = (options) => { 29 | if (typeof options === 'string') { 30 | options = { 31 | message: '温馨提示', 32 | description: options, 33 | } 34 | } 35 | options.type = type 36 | return Notification(options) 37 | } 38 | }, 39 | ) 40 | 41 | export { Message, Notification } 42 | -------------------------------------------------------------------------------- /src/utils/socket.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: chenxin 3 | * @Description: socket封装 4 | */ 5 | export default class Socket { 6 | public url = ''; // socket服务地址 7 | 8 | public callback: { 9 | [key: string]: any 10 | } = {}; // 全局socket回调 11 | 12 | public instance: any = null; // socket实例 13 | 14 | constructor({ url }: { url: string }) { 15 | this.url = url; 16 | this.init(); 17 | } 18 | 19 | // 初始化socket连接 20 | init() { 21 | const instance: WebSocket = new WebSocket(`ws://${this.url}`); 22 | // 接收返回数据 23 | instance.onmessage = (e) => { 24 | const res = JSON.parse(e.data); 25 | this.callback[res.type] 26 | ? this.callback[res.type](res.data) 27 | : console.warn('接收数据失败,无对应回调'); 28 | }; 29 | // 关闭socket连接 30 | instance.onclose = (e) => { 31 | console.warn(`connect closed(${e.code})`); 32 | // 重连websocket 33 | setTimeout(() => { 34 | this.init(); 35 | }, 1000); 36 | }; 37 | // 连接成功 38 | instance.onopen = () => { 39 | console.log('连接成功!'); 40 | }; 41 | // socket连接失败 42 | instance.onerror = () => { 43 | console.warn('websocket连接失败!'); 44 | }; 45 | this.instance = instance; 46 | } 47 | 48 | // 添加回调 49 | addCallback(type: string, callback: any) { 50 | this.callback[type] = callback; 51 | } 52 | 53 | // 移除回调 54 | removeCallback(type: string) { 55 | this.callback[type] 56 | ? delete this.callback[type] 57 | : console.warn('未找到对应回调,无法删除!'); 58 | } 59 | 60 | // 发送数据 61 | send(data: any) { 62 | if (this.instance.readyState == this.instance.OPEN) { 63 | this.instance.send(JSON.stringify(data)); 64 | } else { 65 | setTimeout(() => { 66 | this.send(data); 67 | }, 1000); 68 | } 69 | } 70 | 71 | // 关闭socket连接 72 | close() { 73 | this.instance && this.instance.close(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | // token 2 | export const setToken = (token: string) => sessionStorage.setItem('token', token); 3 | export const getToken = () => sessionStorage.getItem('token'); 4 | export const removeToken = () => sessionStorage.removeItem('token'); 5 | -------------------------------------------------------------------------------- /src/utils/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 工具包 3 | * @Author: chenxin 4 | * @Date: 2020-09-22 16:11:50 5 | * @LastEditors: Please set LastEditors 6 | * @LastEditTime: 2020-11-16 23:13:56 7 | */ 8 | 9 | import CryptoJS from 'crypto-js'; 10 | import SparkMD5 from 'spark-md5'; 11 | 12 | const PublicKey = 'ZHONGGUODIANWANG'; 13 | const IvKey = 'ZHONGGUODIANWANG'; 14 | 15 | /** 16 | * URL地址 17 | * @param {*}s 18 | */ 19 | export const isURL = (s: string): boolean => /^http[s]?:\/\/.*/.test(s); 20 | 21 | /** 22 | * 验证密码强度 23 | * @param value 24 | */ 25 | export const checkPassModes = (value: string): number => { 26 | let modes = 0; 27 | // 正则表达式验证符合要求的 28 | if (value.length < 1) return modes; 29 | if (/\d/.test(value)) modes++; // 数字 30 | if (/[a-z]/.test(value)) modes++; // 小写 31 | if (/[A-Z]/.test(value)) modes++; // 大写 32 | if (/\W/.test(value)) modes++; // 特殊字符 33 | return modes; 34 | }; 35 | 36 | /** 37 | * 判断是否是IE 38 | */ 39 | export const isIE = () => { 40 | const bw = window.navigator.userAgent; 41 | const compare = (s: string) => bw.indexOf(s) >= 0; 42 | const ie11 = (() => 'ActiveXObject' in window)(); 43 | return compare('MSIE') || ie11; 44 | }; 45 | 46 | /* 47 | * ECB加密 48 | * @param {String} word 需要加密的密码 49 | * @param {String} keyStr 对密码加密的秘钥 50 | * @return {String} 加密的密文 51 | * */ 52 | export const ECBEncrypt = (word: string | number, keyStr?: string) => { 53 | keyStr = keyStr || PublicKey; 54 | word = typeof word != 'string' ? word.toString() : word; 55 | const key = CryptoJS.enc.Utf8.parse(keyStr); 56 | const encryptedData = CryptoJS.AES.encrypt(word, key, { 57 | mode: CryptoJS.mode.ECB, 58 | padding: CryptoJS.pad.Pkcs7 59 | }); 60 | return encryptedData.toString(); 61 | }; 62 | 63 | /* 64 | * ECB解密 65 | * @param {String} word 需要加密的密码 66 | * @param {String} keyStr 对密码加密的秘钥 67 | * @return {String} 解密的明文 68 | * */ 69 | export const ECBDecrypt = (word: string | number, keyStr?: string) => { 70 | keyStr = keyStr || PublicKey; 71 | word = typeof word == 'number' ? word.toString() : word; 72 | const key = CryptoJS.enc.Utf8.parse(keyStr); 73 | const encryptedHexStr = CryptoJS.enc.Base64.parse(word); 74 | const encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr); 75 | const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, key, { 76 | mode: CryptoJS.mode.ECB, 77 | padding: CryptoJS.pad.Pkcs7 78 | }); 79 | return decryptedData.toString(CryptoJS.enc.Utf8); 80 | }; 81 | 82 | /* 83 | * CBC加密 84 | * @param {String} word 需要加密的密码 85 | * @param {String} keyStr 对密码加密的秘钥 86 | * @return {String} 加密的密文 87 | * */ 88 | export const CBCEncrypt = (word: string | number, keyStr?: string) => { 89 | keyStr = keyStr || PublicKey; 90 | word = typeof word == 'number' ? word.toString() : word; 91 | const key = CryptoJS.enc.Utf8.parse(keyStr); 92 | const iv = CryptoJS.enc.Utf8.parse(IvKey); 93 | 94 | const srcs = CryptoJS.enc.Utf8.parse(word); 95 | const encrypted = CryptoJS.AES.encrypt(srcs, key, { 96 | iv, 97 | mode: CryptoJS.mode.CBC, 98 | padding: CryptoJS.pad.Pkcs7 99 | }); 100 | return CryptoJS.enc.Base64.stringify(encrypted.ciphertext); 101 | }; 102 | 103 | /* 104 | * CBC解密 105 | * @param {String} word 需要加密的密码 106 | * @param {String} keyStr 对密码加密的秘钥 107 | * @return {String} 解密的明文 108 | * */ 109 | export const CBCDecrypt = (word: string | number, keyStr?: string) => { 110 | keyStr = keyStr || PublicKey; 111 | word = typeof word == 'number' ? word.toString() : word; 112 | const key = CryptoJS.enc.Utf8.parse(keyStr); 113 | const iv = CryptoJS.enc.Utf8.parse(IvKey); 114 | 115 | const encryptedHexStr = CryptoJS.enc.Base64.parse(word); 116 | const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr); 117 | const decrypt = CryptoJS.AES.decrypt(srcs, key, { 118 | iv, 119 | mode: CryptoJS.mode.CBC, 120 | padding: CryptoJS.pad.Pkcs7 121 | }); 122 | const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); 123 | return decryptedStr.toString(); 124 | }; 125 | 126 | /** 127 | * 获取上传文件md5 128 | * @param {*} dataFile 129 | */ 130 | export const getUploadFileMd5 = (dataFile: Blob) => new Promise((resolve) => { 131 | const fileReader = new FileReader(); 132 | const spark = new SparkMD5.ArrayBuffer(); 133 | // 获取文件二进制数据 134 | fileReader.readAsArrayBuffer(dataFile); 135 | fileReader.onload = (e: ProgressEvent) => { 136 | spark.append((e.target as any).result); 137 | const md5 = spark.end(); 138 | return resolve(ECBEncrypt(md5)); 139 | }; 140 | }); 141 | 142 | /** 143 | * 获取上传图片base64 144 | * @param file 145 | */ 146 | export const getFileBase64 = (file: Blob) => new Promise((resolve, reject) => { 147 | const fileReader: any = new FileReader(); 148 | fileReader.readAsDataURL(file); 149 | fileReader.onload = () => { 150 | const image = new Image(); 151 | image.src = fileReader.result; 152 | image.onload = () => resolve({ 153 | base64: fileReader.result, 154 | width: image.width, 155 | height: image.height 156 | }); 157 | }; 158 | fileReader.onerror = (error: any) => reject(error); 159 | }); 160 | 161 | /** 162 | * 创建formData参数(只针对有图片上传) 163 | * @param {*} params 164 | */ 165 | export function createdFormData(params: any) { 166 | const formData = new FormData(); 167 | Object.keys(params).forEach((key) => { 168 | if (params[key] == 0 || (params[key] != '' && params[key] != undefined && params[key] != null)) { 169 | if (params[key] instanceof Array) { 170 | params[key].forEach((item: any) => { 171 | formData.append(`${key}[]`, item); 172 | }); 173 | } else { 174 | formData.append(key, params[key]); 175 | } 176 | } 177 | }); 178 | return formData; 179 | } 180 | 181 | // 判断某个dom元素是否包含在另一个dom元素中 182 | export const contains = (parent: any, node: any) => { 183 | parent !== node && parent.contains(node); 184 | }; 185 | 186 | // 格式化边界点位数据为二维数组 187 | export const formatPolylinePoint = (polyline: any) => { 188 | const arr: any = []; 189 | polyline?.split(';').map((item: any) => { 190 | const point = item.split(',').map(Number); 191 | const flag = point.includes(NaN); 192 | if (!flag) { 193 | arr.push(point); 194 | } 195 | }); 196 | return arr; 197 | }; 198 | 199 | // 生成uuid 200 | export const uuid = () => { 201 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 202 | var r = Math.random() * 16 | 0, 203 | v = c == 'x' ? r : (r & 0x3 | 0x8); 204 | return v.toString(16); 205 | }); 206 | } -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 验证库 3 | * @Author: chenxin 4 | * @Date: 2020-09-28 16:13:12 5 | * @LastEditors: Please set LastEditors 6 | * @LastEditTime: 2020-11-16 23:18:30 7 | */ 8 | 9 | // 手机号验证 10 | export const validateMobile = (value: string) => 11 | /^[1][3,4,5,7,8,9][0-9]{9}$/.test(value) 12 | 13 | // 身份证验证 14 | export const validateCard = (value: string) => 15 | /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value) 16 | 17 | // 验证年龄 18 | export const validateAge = (value: string) => 19 | /^(([0-9]|[1-9][1-9]|1[0-7][0-9])(\\.[0-9]+)?|180)$/.test(value) 20 | 21 | // 微信号验证 22 | export const validatWeChat = (value: string) => 23 | /^[a-zA-Z][a-zA-Z\d_-]{5,19}$/.test(value) 24 | 25 | // 中文验证 26 | export const validateEnName = (value: string) => 27 | /^[\u4e00-\u9fa5]{0,10}$/.test(value) 28 | 29 | // 验证IP 30 | export const validateIP = (value: string) => 31 | /^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test( 32 | value, 33 | ) 34 | 35 | // 验证端口 为正整数,值范围1-65535 36 | export const validatePort = (value: string) => 37 | /(^[1-9]\d{0,3}$)|(^[1-5]\d{4}$)|(^6[0-4]\d{3}$)|(^65[0-4]\d{2}$)|(^655[0-2]\d$)|(^6553[0-5]$)/.test( 38 | value, 39 | ) 40 | 41 | // 服务地址限定条件 英文、数字、:、/、.、@、_、#、*,长度为1-256 42 | export const validateAddress = (value: string) => 43 | /^[A-Za-z0-9:/@._#*]{1,256}$/.test(value) 44 | 45 | // 验证1到99999999正整数 46 | export const validateRate = (value: string) => /^[1-9][0-9]{0,7}$/.test(value) 47 | 48 | // 经度验证 49 | export const validateLon = (value: string) => 50 | /^[\-\+]?(0(\.\d{1,10})?|([1-9](\d)?)(\.\d{1,10})?|1[0-7]\d{1}(\.\d{1,10})?|180\.0{1,10})$/.test( 51 | value, 52 | ) 53 | 54 | // 纬度验证 55 | export const validateLat = (value: string) => 56 | /^[\-\+]?((0|([1-8]\d?))(\.\d{1,10})?|90(\.0{1,10})?)$/.test(value) 57 | 58 | // 邮箱验证 59 | export const validateEmail = (value: string) => 60 | /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/.test( 61 | value, 62 | ) 63 | 64 | // 正整数 65 | export const validatePort100 = (value: string) => /^[1-9]\d*$/.test(value) 66 | 67 | // 非英文数字验证 68 | export const validateNotEnglishAndNumbers = (value: string) => 69 | /^[^\d]*$|^[^a-zA-Z]*$|[^\da-zA-Z]/.test(value) 70 | -------------------------------------------------------------------------------- /src/views/dataManage/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/views/dataManage/index.module.less -------------------------------------------------------------------------------- /src/views/dataManage/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouteLayout } from '@/layouts' 3 | 4 | import styles from './index.module.less' 5 | 6 | const DataManage = defineComponent({ 7 | name: 'DataManage', 8 | setup() { 9 | const slots = { 10 | default: () =>
数据管理
, 11 | } 12 | return () => 13 | }, 14 | }) 15 | 16 | export default DataManage 17 | -------------------------------------------------------------------------------- /src/views/dataProtal/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/views/dataProtal/index.module.less -------------------------------------------------------------------------------- /src/views/dataProtal/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouteLayout } from '@/layouts' 3 | 4 | import styles from './index.module.less' 5 | 6 | const DataProtal = defineComponent({ 7 | name: 'DataProtal', 8 | setup() { 9 | const slots = { 10 | default: () =>
数据门户
, 11 | } 12 | return () => 13 | }, 14 | }) 15 | 16 | export default DataProtal 17 | -------------------------------------------------------------------------------- /src/views/designCenter/materialMange/customControl/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/views/designCenter/materialMange/customControl/index.module.less -------------------------------------------------------------------------------- /src/views/designCenter/materialMange/customControl/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouteLayout } from '@/layouts' 3 | 4 | import styles from './index.module.less' 5 | 6 | const CustomControl = defineComponent({ 7 | name: 'CustomControl', 8 | setup() { 9 | return () =>
自定义控件
10 | }, 11 | }) 12 | 13 | export default CustomControl 14 | -------------------------------------------------------------------------------- /src/views/designCenter/materialMange/customMaterial/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/views/designCenter/materialMange/customMaterial/index.module.less -------------------------------------------------------------------------------- /src/views/designCenter/materialMange/customMaterial/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouteLayout } from '@/layouts' 3 | 4 | import styles from './index.module.less' 5 | 6 | const CustomMaterial = defineComponent({ 7 | name: 'CustomMaterial', 8 | setup() { 9 | return () =>
自定义素材
10 | }, 11 | }) 12 | 13 | export default CustomMaterial 14 | -------------------------------------------------------------------------------- /src/views/designCenter/screenManage/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/views/designCenter/screenManage/index.module.less -------------------------------------------------------------------------------- /src/views/designCenter/screenManage/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouteLayout } from '@/layouts' 3 | 4 | import styles from './index.module.less' 5 | 6 | const ScreenManage = defineComponent({ 7 | name: 'ScreenManage', 8 | setup() { 9 | return () =>
画面管理
10 | }, 11 | }) 12 | 13 | export default ScreenManage 14 | -------------------------------------------------------------------------------- /src/views/exception/403.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import ExceptionPage from '@/components/Exception' 3 | 4 | const Exception403 = defineComponent({ 5 | name: 'Exception403', 6 | setup() { 7 | return () => 8 | }, 9 | }) 10 | 11 | export default Exception403 12 | -------------------------------------------------------------------------------- /src/views/exception/404.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import ExceptionPage from '@/components/Exception' 3 | 4 | const Exception404 = defineComponent({ 5 | name: 'Exception404', 6 | setup() { 7 | return () => 8 | }, 9 | }) 10 | 11 | export default Exception404 12 | -------------------------------------------------------------------------------- /src/views/exception/500.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import ExceptionPage from '@/components/Exception' 3 | 4 | const Exception500 = defineComponent({ 5 | name: 'Exception500', 6 | setup() { 7 | return () => 8 | }, 9 | }) 10 | 11 | export default Exception500 12 | -------------------------------------------------------------------------------- /src/views/home/index.module.less: -------------------------------------------------------------------------------- 1 | .home { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent } from 'vue' 2 | import { useStore } from 'vuex' 3 | import { key } from '@/store' 4 | 5 | import styles from './index.module.less' 6 | 7 | const Home = defineComponent({ 8 | name: 'Home', 9 | setup(props) { 10 | const store = useStore(key) 11 | 12 | const count = computed(() => { 13 | return store.getters.getCount 14 | }) 15 | const addCountNum = () => { 16 | store.commit('setCountNum') 17 | } 18 | return () => ( 19 |
20 | 21 |
22 | ) 23 | }, 24 | }) 25 | 26 | export default Home 27 | -------------------------------------------------------------------------------- /src/views/user/login/index.module.less: -------------------------------------------------------------------------------- 1 | .site-wrapper-login { 2 | .site-content__wrapper { 3 | margin: 0 auto; 4 | align-self: center; 5 | .site-content { 6 | min-height: 100%; 7 | padding: 30px; 8 | } 9 | .login-main { 10 | padding: 10px; 11 | min-height: 100%; 12 | min-width: 520px; 13 | .img { 14 | width: 500px; 15 | margin-bottom: 46px; 16 | } 17 | } 18 | .login-btn-submit { 19 | width: 100%; 20 | margin-top: 15px; 21 | } 22 | .captcha-row { 23 | display: flex; 24 | flex-flow: row nowrap; 25 | align-items: center; 26 | .login-captcha { 27 | overflow: hidden; 28 | > img { 29 | width: 100%; 30 | cursor: pointer; 31 | } 32 | } 33 | } 34 | } 35 | .notice { 36 | position: absolute; 37 | bottom: 10px; 38 | font-size: 13px; 39 | color: rgba(255, 255, 255, 0.8); 40 | width: 100%; 41 | text-align: center; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/views/user/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, reactive, ref, onMounted } from 'vue' 2 | import { useRouter } from 'vue-router' 3 | import { Form, Input, Button, Row, Col, message } from 'ant-design-vue' 4 | import { uuid } from '@/utils/util' 5 | import GlobalBg from '@/components/GlobalBg' 6 | import UserApi from '@/api/user.ts' 7 | 8 | import styles from './index.module.less' 9 | 10 | const Login = defineComponent({ 11 | name: 'Login', 12 | setup(props) { 13 | const router = useRouter() 14 | const labelCol = { span: 4 } 15 | const wrapperCol = { span: 18, offset: 3 } 16 | const formRef = ref() 17 | 18 | let captchaPath = ref('') // 图片验证码地址 19 | 20 | let loading = ref(false) // 提交状态 21 | 22 | const formData = reactive({ 23 | username: '', // 用户名 24 | password: '', // 密码 25 | captcha: '', // 验证码 26 | uuid: '', // 验证码uuid 27 | }) // form表单数据 28 | 29 | const rules = { 30 | username: [ 31 | { required: true, message: '用户名不能为空', trigger: 'blur' }, 32 | ], 33 | password: [{ required: true, message: '密码不能为空', trigger: 'blur' }], 34 | captcha: [{ required: true, message: '验证码不能为空', trigger: 'blur' }], 35 | } // form表单验证规则 36 | 37 | // 随机验证码 38 | const getCaptcha = () => { 39 | formData.uuid = uuid() 40 | captchaPath.value = `/dbd-authority/captcha.jpg?uuid=${formData.uuid}` 41 | } 42 | 43 | // form表单提交 44 | const handleFormSubmit = async () => { 45 | try { 46 | await formRef.value.validate() 47 | loading.value = true 48 | try { 49 | const { data } = await UserApi.login(formData) 50 | sessionStorage.setItem('token', data.token) 51 | sessionStorage.setItem('userInfo', JSON.stringify(data.userInfo)) 52 | router.push('/dataProtal') 53 | loading.value = false 54 | } catch (error) { 55 | loading.value = false 56 | getCaptcha() 57 | } 58 | } catch (error) { 59 | message.warning('账号:admin 密码:666666') 60 | console.log(error) 61 | } 62 | } 63 | 64 | onMounted(() => { 65 | getCaptcha() 66 | }) 67 | 68 | const slots = { 69 | content: () => ( 70 |
71 |
72 |
73 | 74 |
81 | 82 | 87 | 88 | 89 | 95 | 96 | {/* 97 | 98 | 99 | 104 | 105 | 106 | 111 | 112 | 113 | */} 114 | 115 | 124 | 125 |
126 |
127 |
128 |
129 | ), 130 | footer: () => ( 131 |

132 | 建议使用Chrome浏览器(版本: 133 | 84.0.4147.89 134 | 及以上)在分辨率为 135 | 1920x1080 136 | 下访问本平台 137 |

138 | ), 139 | } 140 | return () => ( 141 | 142 | ) 143 | }, 144 | }) 145 | 146 | export default Login 147 | -------------------------------------------------------------------------------- /src/views/userManage/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunshine824/vue3.0-typescript-starter/c34a4d062379a6e5f35390fcb59ef5d4de2629ae/src/views/userManage/index.module.less -------------------------------------------------------------------------------- /src/views/userManage/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { RouteLayout } from '@/layouts' 3 | 4 | import styles from './index.module.less' 5 | 6 | const UserManage = defineComponent({ 7 | name: 'userManage', 8 | setup() { 9 | const slots = { 10 | default: () =>
用户管理
, 11 | } 12 | return () => 13 | }, 14 | }) 15 | 16 | export default UserManage 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["esnext", "dom"], 12 | "baseUrl": "./", 13 | "paths": { 14 | "@/*": ["*","src/*"] 15 | } 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/App.vue", "src/utils/resetMessage.js"] 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import path from 'path' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import styleImport from 'vite-plugin-style-import' 6 | import viteCompression from 'vite-plugin-compression' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | resolve: { 11 | alias: { 12 | '@': path.join(__dirname, './src'), 13 | '@views': path.join(__dirname, './src/views'), 14 | '@components': path.join(__dirname, './src/components'), 15 | '@utils': path.join(__dirname, './src/utils'), 16 | '@public': path.join(__dirname, './src/public'), 17 | }, 18 | }, 19 | esbuild: { 20 | jsxFactory: 'h', 21 | jsxFragment: 'Fragment', 22 | jsxInject: "import { h } from 'vue';", 23 | }, 24 | build: { 25 | chunkSizeWarningLimit: 500, 26 | minify: 'terser', 27 | cssCodeSplit: true, // 如果设置为false,整个项目中的所有 CSS 将被提取到一个 CSS 文件中 28 | terserOptions: { 29 | compress: { 30 | drop_console: true, //打包时删除console 31 | drop_debugger: true, //打包时删除 debugger 32 | pure_funcs: ['console.log'], 33 | }, 34 | }, 35 | rollupOptions: { 36 | output: { 37 | manualChunks: { 38 | // 拆分代码,这个就是分包,配置完后自动按需加载,现在还比不上webpack的splitchunk,不过也能用了。 39 | vue: ['vue', 'vue-router', 'vuex'], 40 | 'ant-design-vue': ['ant-design-vue'], 41 | }, 42 | }, 43 | }, 44 | brotliSize: false, 45 | }, 46 | plugins: [ 47 | vue(), 48 | vueJsx(), 49 | styleImport({ 50 | libs: [ 51 | { 52 | libraryName: 'ant-design-vue', 53 | esModule: true, 54 | resolveStyle: (name) => { 55 | return `ant-design-vue/es/${name}/style/index` 56 | }, 57 | }, 58 | ], 59 | }), 60 | // 打包压缩,主要是本地gzip,如果服务器配置压缩也可以 61 | viteCompression(), 62 | ], 63 | css: { 64 | preprocessorOptions: { 65 | less: { 66 | modifyVars: { 67 | // 更改主题在这里 68 | 'link-color': '#1DA57A', 69 | 'border-radius-base': '2px', 70 | }, 71 | javascriptEnabled: true, 72 | }, 73 | }, 74 | }, 75 | server: { 76 | host: '0.0.0.0', 77 | port: 4000, // 设置服务启动端口号 78 | open: false, // 设置服务启动时是否自动打开浏览器 79 | https: false, 80 | cors: true, // 允许跨域 81 | 82 | // 设置代理,根据我们项目实际情况配置 83 | proxy: { 84 | '/dbd-authority': { 85 | target: 'http://192.168.1.11:9000/dbd-authority', 86 | changeOrigin: true, 87 | secure: false, 88 | rewrite: (path) => path.replace('/dbd-authority/', ''), 89 | }, 90 | }, 91 | }, 92 | }) 93 | --------------------------------------------------------------------------------