├── .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 | 
277 |
278 | ## Fetch
279 | 为了更好的支持TypeScript,统计Api请求,这里将axios进行二次封装
280 |
281 | 结构目录:
282 |
283 | 
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 | 
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 |
544 | {{t('menu.userManage')}}
545 |
546 |
551 | ```
552 | - globalInjection:true
553 | ```
554 |
555 | {{$t('menu.userManage')}}
556 |
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 | 
703 | - BlankLayout.tsx: 空白布局,只做路由分发
704 | - RouteLayout.tsx: 主体布局,内容显示部分,包含面包屑
705 | - LevelBasicLayout.tsx 多级展示布局,适用于2级以上路由
706 | 
707 | 
708 | - SimplifyBasicLayout.tsx 简化版多级展示布局,适用于2级以上路由
709 | 
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 |
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 |
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 |
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 |
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 |
66 | ),
67 | }
68 |
69 | const slots = {
70 | content: () => (
71 | <>
72 | {/* 导航栏 */}
73 |
74 | {/* 用户信息 */}
75 |
83 | >
84 | ),
85 | }
86 | return () => (
87 |
88 |
89 |
90 | {/* 侧边子目录 */}
91 | {subMenuLists['value'].length ? (
92 |
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 |
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 |
44 | ),
45 | }
46 |
47 | const slots = {
48 | content: () => (
49 | <>
50 | {/* 用户信息 */}
51 |
59 | >
60 | ),
61 | }
62 | return () => (
63 |
64 |
65 |
66 | {/* 导航栏 */}
67 |
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 |
43 |
44 |
45 | - Unicode
46 | - Font class
47 | - Symbol
48 |
49 |
50 |
查看项目
51 |
52 |
53 |
54 |
55 |
56 |
57 | -
58 |
59 |
设计师2
60 | 
61 |
62 |
63 | -
64 |
65 |
基础控件图标-公共选择框
66 | 
67 |
68 |
69 | -
70 |
71 |
自定义
72 | 
73 |
74 |
75 | -
76 |
77 |
素材
78 | 
79 |
80 |
81 | -
82 |
83 |
用户管理
84 | 
85 |
86 |
87 | -
88 |
89 |
【视图】主视图
90 | 
91 |
92 |
93 | -
94 |
95 |
首页-选中
96 | 
97 |
98 |
99 | -
100 |
101 |
数据
102 | 
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">3</span>
142 |
143 |
144 | "iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。
145 |
146 |
147 |
148 |
149 |
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 |
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 |
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 |
--------------------------------------------------------------------------------