vue3+TypeScript项目实战硅谷甄选

技术栈包含: vue3+TypeScript+vue-router+pinia+element-plus+axios+echarts***等技术栈。

一、vue3组件通信方式

比如:vue2组件通信方式

**props:**可以实现父子组件、子父组件、甚至兄弟组件通信

自定义事件:可以实现子父组件通信

全局事件总线$bus:可以实现任意组件通信

**pubsub:**发布订阅模式实现任意组件通信

vuex:集中式状态管理容器,实现任意组件通信

ref:父组件获取子组件实例VC,获取子组件的响应式数据以及方法

**slot:**插槽(默认插槽、具名插槽、作用域插槽)实现父子组件通信…

1.1props

props可以实现父子组件通信,在vue3中我们可以通过defineProps获取父组件传递的数据。且在组件内部不需要引入defineProps方法可以直接使用!

父组件给子组件传递数据

<Child info="我爱祖国" :money="money"></Child>

子组件获取父组件传递数据:方式1

let props = defineProps({
  info:{
   type:String,//接受的数据类型
   default:'默认参数',//接受默认数据
  },
  money:{
   type:Number,
   default:0
}})

子组件获取父组件传递数据:方式2

let props = defineProps(["info",'money']);

子组件获取到props数据就可以在模板中使用了,但是切记props是只读的(只能读取,不能修改)
在这里插入图片描述

1.2自定义事件

在vue框架中事件分为两种:一种是原生的DOM事件,另外一种自定义事件。

原生DOM事件可以让用户与网页进行交互,比如click、dbclick、change、mouseenter、mouseleave…

自定义事件可以实现子组件给父组件传递数据

1.2.1原生DOM事件

代码如下:

 <pre @click="handler">
      我是祖国的老花骨朵
 </pre>

当前代码级给pre标签绑定原生DOM事件点击事件,默认会给事件回调注入event事件对象。当然点击事件想注入多个参数可以按照下图操作。但是切记注入的事件对象务必叫做$event.

  <div @click="handler1(1,2,3,$event)">我要传递多个参数</div>

在vue3框架click、dbclick、change(这类原生DOM事件),不管是在标签、自定义标签上(组件标签)都是原生DOM事件。
在这里插入图片描述

1.2.2自定义事件

自定义事件可以实现子组件给父组件传递数据.在项目中是比较常用的。

比如在父组件内部给子组件(Event2)绑定一个自定义事件

<Event2  @xxx="handler3"></Event2>

在Event2子组件内部触发这个自定义事件

<template>
  <div>
    <h1>我是子组件2</h1>
    <button @click="handler">点击我触发xxx自定义事件</button>
  </div>
</template>

<script setup lang="ts">
let $emit = defineEmits(["xxx"]);
const handler = () => {
  $emit("xxx", "法拉利", "茅台");
};
</script>
<style scoped>
</style>

我们会发现在script标签内部,使用了defineEmits方法,此方法是vue3提供的方法,不需要引入直接使用。defineEmits方法执行,传递一个数组,数组元素即为将来组件需要触发的自定义事件类型,此方执行会返回一个$emit方法用于触发自定义事件。

当点击按钮的时候,事件回调内部调用$emit方法去触发自定义事件,第一个参数为触发事件类型,第二个、三个、N个参数即为传递给父组件的数据。

需要注意的是:代码如下

<Event2  @xxx="handler3" @click="handler"></Event2>

正常来说组件标签书写@click应该为原生DOM事件,但是如果子组件内部通过defineEmits定义就变为自定义事件了

let $emit = defineEmits(["xxx",'click']);

1.3全局事件总线

全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系推出全局事件总线。

但是在vue3中没有Vue构造函数,也就没有Vue.prototype.以及组合式API写法没有this,

那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能可以使用插件mitt实现。

mitt:官网地址:https://www.npmjs.com/package/mitt
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
mitt兄弟组件通信
在这里插入图片描述
在这里插入图片描述

1.4 v-model

v-model指令可以收集表单数据(数据双向绑定),除此之外它也可以实现父子组件数据同步。

而v-model实指利用props[modelValue]与自定义事件[update:modelValue]实现的。

下方代码:相当于给组件Child传递一个props(modelValue)与绑定一个自定义事件update:modelValue

实现父子组件数据同步

<Child v-model="msg"></Child>

在vue3中一个组件可以通过使用多个v-model,让父子组件多个数据同步,下方代码相当于给组件Child传递两个props分别是pageNo与pageSize,以及绑定两个自定义事件update:pageNo与update:pageSize实现父子数据同步

<Child v-model:pageNo="msg" v-model:pageSize="msg1"></Child>

父组件
在这里插入图片描述
子组件
在这里插入图片描述

1.5useAttrs

在Vue3中可以利用useAttrs方法获取组件的属性与事件(包含:原生DOM事件或者自定义事件),次函数功能类似于Vue2框架中 a t t r s 属性与 attrs属性与 attrs属性与listeners方法。

比如:在父组件内部使用一个子组件my-button

<my-button type="success" size="small" title='标题' @click="handler"></my-button>

子组件内部可以通过useAttrs方法获取组件属性与事件.因此你也发现了,它类似于props,可以接受父组件传递过来的属性与属性值。需要注意如果defineProps接受了某一个属性,useAttrs方法返回的对象身上就没有相应属性与属性值。

<script setup lang="ts">
import {useAttrs} from 'vue';
let $attrs = useAttrs();
</script>

打印$attrs,是一个代理对象,里面包含父组件传递的属性和方法(未用props接收的)
在这里插入图片描述
在这里插入图片描述

1.6 ref与$parent

ref,提及到ref可能会想到它可以获取元素的DOM或者获取子组件实例的VC。既然可以在父组件内部通过ref获取子组件实例VC,那么子组件内部的方法与响应式数据父组件可以使用的。

比如:在父组件挂载完毕获取组件实例

父组件内部代码:

<template>
  <div>
    <h1>ref与$parent</h1>
    <Son ref="son"></Son>
  </div>
</template>
<script setup lang="ts">
import Son from "./Son.vue";
import { onMounted, ref } from "vue";
const son = ref();
onMounted(() => {
  console.log(son.value);
});
</script>

但是需要注意,如果想让父组件获取子组件的数据或者方法需要通过defineExpose对外暴露,因为vue3中组件内部的数据对外“关闭的”,外部不能访问

<script setup lang="ts">
import { ref } from "vue";
//数据
let money = ref(1000);
//方法
const handler = ()=>{
}
defineExpose({
  money,
   handler
})
</script>

$parent可以获取某一个组件的父组件实例VC,因此可以使用父组件内部的数据与方法。必须子组件内部拥有一个按钮点击时候获取父组件实例,当然父组件的数据与方法需要通过defineExpose方法对外暴露

<button @click="handler($parent)">点击我获取父组件实例</button>

在这里插入图片描述

1.7 provide与inject

provide[提供]

inject[注入]

vue3提供两个方法provide与inject,可以实现隔辈组件传递参数

组件组件提供数据:

provide方法用于提供数据,此方法执需要传递两个参数,分别提供数据的key与提供数据value

<script setup lang="ts">
import {provide} from 'vue'
provide('token','admin_token');
</script>

后代组件可以通过inject方法获取数据,通过key获取存储的数值

<script setup lang="ts">
import {inject} from 'vue'
let token = inject('token');
</script>

后代组件可以修改祖先组件传来的数据
在这里插入图片描述

1.8 pinia

pinia官网:https://pinia.web3doc.top/

pinia也是集中式管理状态容器,类似于vuex。但是核心概念没有mutation、modules,使用方式参照官网

在这里插入图片描述

1.9 slot

插槽:默认插槽、具名插槽、作用域插槽可以实现父子组件通信.

默认插槽:

在子组件内部的模板中书写slot全局组件标签

<template>
  <div>
    <slot></slot>
  </div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

在父组件内部提供结构:Todo即为子组件,在父组件内部使用的时候,在双标签内部书写结构传递给子组件

注意开发项目的时候默认插槽一般只有一个

<Todo>
  <h1>我是默认插槽填充的结构</h1>
</Todo>

具名插槽:

顾名思义,此插槽带有名字在组件内部留多个指定名字的插槽。

下面是一个子组件内部,模板中留两个插槽

<template>
  <div>
    <h1>todo</h1>
    <slot name="a"></slot>
    <slot name="b"></slot>
  </div>
</template>
<script setup lang="ts">
</script>

<style scoped>
</style>

父组件内部向指定的具名插槽传递结构。需要注意v-slot:可以替换为#

<template>
  <div>
    <h1>slot</h1>
    <Todo>
      <template v-slot:a>  //可以用#a替换
        <div>填入组件A部分的结构</div>
      </template>
      <template v-slot:b>//可以用#b替换
        <div>填入组件B部分的结构</div>
      </template>
    </Todo>
  </div>
</template>

<script setup lang="ts">
import Todo from "./Todo.vue";
</script>
<style scoped>
</style>

作用域插槽

作用域插槽:可以理解为,子组件数据由父组件提供,但是子组件内部决定不了自身结构与外观(样式)

子组件Todo代码如下:

<template>
  <div>
    <h1>todo</h1>
    <ul>
     <!--组件内部遍历数组-->
      <li v-for="(item,index) in todos" :key="item.id">
         <!--作用域插槽将数据回传给父组件-->
         <slot :$row="item" :$index="index"></slot>
      </li>
    </ul>
  </div>
</template>
<script setup lang="ts">
defineProps(['todos']);//接受父组件传递过来的数据
</script>
<style scoped>
</style>

父组件内部代码如下:

<template>
  <div>
    <h1>slot</h1>
    <Todo :todos="todos">
      <template v-slot="{$row,$index}">
         <!--父组件决定子组件的结构与外观-->
         <span :style="{color:$row.done?'green':'red'}">{{$row.title}}</span>
      </template>
    </Todo>
  </div>
</template>

<script setup lang="ts">
import Todo from "./Todo.vue";
import { ref } from "vue";
//父组件内部数据
let todos = ref([
  { id: 1, title: "吃饭", done: true },
  { id: 2, title: "睡觉", done: false },
  { id: 3, title: "打豆豆", done: true },
]);
</script>
<style scoped>
</style>

二、搭建后台管理系统模板

2.1项目初始化

今天来带大家从0开始搭建一个vue3版本的后台管理系统。一个项目要有统一的规范,需要使用eslint+stylelint+prettier来对我们的代码质量做检测和修复,需要使用husky来做commit拦截,需要使用commitlint来统一提交规范,需要使用preinstall来统一包管理工具。

下面我们就用这一套规范来初始化我们的项目,集成一个规范的模版。

2.1.1环境准备
  • node v16.14.2
  • pnpm 8.0.0
2.1.2初始化项目

本项目使用vite进行构建,vite官方中文文档参考:cn.vitejs.dev/guide/

pnpm:performant npm ,意味“高性能的 npm”。pnpm由npm/yarn衍生而来,解决了npm/yarn内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为“最先进的包管理工具”

pnpm安装指令

npm i -g pnpm

项目初始化命令:

pnpm create vite

进入到项目根目录pnpm install安装全部依赖.安装完依赖运行程序:pnpm run dev

运行完毕项目跑在http://127.0.0.1:5173/,可以访问你得项目啦

加–open自动打开浏览器
在这里插入图片描述

2.2项目配置

一、eslint配置

eslint中文官网:http://eslint.cn/

ESLint最初是由Nicholas C. Zakas 于2013年6月创建的开源项目。它的目标是提供一个插件化的javascript代码检测工具

首先安装eslint

pnpm i eslint -D

生成配置文件:.eslint.cjs

npx eslint --init

.eslint.cjs配置文件

module.exports = {
   //运行环境
    "env": { 
        "browser": true,//浏览器端
        "es2021": true,//es2021
    },
    //规则继承
    "extends": [ 
       //全部规则默认是关闭的,这个配置项开启推荐规则,推荐规则参照文档
       //比如:函数不能重名、对象不能出现重复key
        "eslint:recommended",
        //vue3语法规则
        "plugin:vue/vue3-essential",
        //ts语法规则
        "plugin:@typescript-eslint/recommended"
    ],
    //要为特定类型的文件指定处理器
    "overrides": [
    ],
    //指定解析器:解析器
    //Esprima 默认解析器
    //Babel-ESLint babel解析器
    //@typescript-eslint/parser ts解析器
    "parser": "@typescript-eslint/parser",
    //指定解析器选项
    "parserOptions": {
        "ecmaVersion": "latest",//校验ECMA最新版本
        "sourceType": "module"//设置为"script"(默认),或者"module"代码在ECMAScript模块中
    },
    //ESLint支持使用第三方插件。在使用插件之前,您必须使用npm安装它
    //该eslint-plugin-前缀可以从插件名称被省略
    "plugins": [
        "vue",
        "@typescript-eslint"
    ],
    //eslint规则
    "rules": {
    }
}
1.1vue3环境代码校验插件
# 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
# 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",
# vue.js的Eslint插件(查找vue语法错误,发现错误指令,查找违规风格指南
"eslint-plugin-vue": "^9.9.0",
# 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",

安装指令

pnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
1.2修改.eslintrc.cjs配置文件
// @see https://eslint.bootcss.com/docs/rules/

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
    jest: true,
  },
  /* 指定如何解析语法 */
  parser: 'vue-eslint-parser',
  /** 优先级低于 parse 的语法解析配置 */
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    parser: '@typescript-eslint/parser',
    jsxPragma: 'React',
    ecmaFeatures: {
      jsx: true,
    },
  },
  /* 继承已有的规则 */
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-essential',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  plugins: ['vue', '@typescript-eslint'],
  /*
   * "off" 或 0    ==>  关闭规则
   * "warn" 或 1   ==>  打开的规则作为警告(不影响代码执行)
   * "error" 或 2  ==>  规则作为一个错误(代码不能执行,界面报错)
   */
  rules: {
    // eslint(https://eslint.bootcss.com/docs/rules/)
    'no-var': 'error', // 要求使用 let 或 const 而不是 var
    'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-unexpected-multiline': 'error', // 禁止空余的多行
    'no-useless-escape': 'off', // 禁止不必要的转义字符

    // typeScript (https://typescript-eslint.io/rules)
    '@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
    '@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
    '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
    '@typescript-eslint/semi': 'off',

    // eslint-plugin-vue (https://eslint.vuejs.org/rules/)
    'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
    'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
    'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
    'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
  },
}

1.3.eslintignore忽略文件
dist
node_modules
1.4运行脚本

package.json新增两个运行脚本

"scripts": {
    "lint": "eslint src",
    "fix": "eslint src --fix",
}
二、配置prettier

有了eslint,为什么还要有prettier?eslint针对的是javascript,他是一个检测工具,包含js语法以及少部分格式问题,在eslint看来,语法对了就能保证代码正常运行,格式问题属于其次;

而prettier属于格式化工具,它看不惯格式不统一,所以它就把eslint没干好的事接着干,另外,prettier支持

包含js在内的多种语言。

总结起来,eslint和prettier这俩兄弟一个保证js代码质量,一个保证代码美观。

2.1安装依赖包
pnpm install -D eslint-plugin-prettier prettier eslint-config-prettier
2.2.prettierrc.json添加规则
{
  "singleQuote": true,
  "semi": false,
  "bracketSpacing": true,
  "htmlWhitespaceSensitivity": "ignore",
  "endOfLine": "auto",
  "trailingComma": "all",
  "tabWidth": 2
}
2.3.prettierignore忽略文件
/dist/*
/html/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*

通过pnpm run lint去检测语法,如果出现不规范格式,通过pnpm run fix 修改

三、配置stylelint

stylelint为css的lint工具。可格式化css代码,检查css语法错误与不合理的写法,指定css书写顺序等。

我们的项目中使用scss作为预处理器,安装以下依赖:

pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D
3.1.stylelintrc.cjs配置文件

官网:https://stylelint.bootcss.com/

// @see https://stylelint.bootcss.com/

module.exports = {
  extends: [
    'stylelint-config-standard', // 配置stylelint拓展插件
    'stylelint-config-html/vue', // 配置 vue 中 template 样式格式化
    'stylelint-config-standard-scss', // 配置stylelint scss插件
    'stylelint-config-recommended-vue/scss', // 配置 vue 中 scss 样式格式化
    'stylelint-config-recess-order', // 配置stylelint css属性书写顺序插件,
    'stylelint-config-prettier', // 配置stylelint和prettier兼容
  ],
  overrides: [
    {
      files: ['**/*.(scss|css|vue|html)'],
      customSyntax: 'postcss-scss',
    },
    {
      files: ['**/*.(html|vue)'],
      customSyntax: 'postcss-html',
    },
  ],
  ignoreFiles: [
    '**/*.js',
    '**/*.jsx',
    '**/*.tsx',
    '**/*.ts',
    '**/*.json',
    '**/*.md',
    '**/*.yaml',
  ],
  /**
   * null  => 关闭该规则
   * always => 必须
   */
  rules: {
    'value-keyword-case': null, // 在 css 中使用 v-bind,不报错
    'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
    'function-url-quotes': 'always', // 要求或禁止 URL 的引号 "always(必须加上引号)"|"never(没有引号)"
    'no-empty-source': null, // 关闭禁止空源码
    'selector-class-pattern': null, // 关闭强制选择器类名的格式
    'property-no-unknown': null, // 禁止未知的属性(true 为不允许)
    'block-opening-brace-space-before': 'always', //大括号之前必须有一个空格或不能有空白符
    'value-no-vendor-prefix': null, // 关闭 属性值前缀 --webkit-box
    'property-no-vendor-prefix': null, // 关闭 属性前缀 -webkit-mask
    'selector-pseudo-class-no-unknown': [
      // 不允许未知的选择器
      true,
      {
        ignorePseudoClasses: ['global', 'v-deep', 'deep'], // 忽略属性,修改element默认样式的时候能使用到
      },
    ],
  },
}
3.2.stylelintignore忽略文件
/node_modules/*
/dist/*
/html/*
/public/*
3.3运行脚本
"scripts": {
	"lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
}

最后配置统一的prettier来格式化我们的js和css,html代码

 "scripts": {
    "dev": "vite --open",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src",
    "fix": "eslint src --fix",
    "format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
    "lint:eslint": "eslint src/**/*.{ts,vue} --cache --fix",
    "lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
  },

当我们运行pnpm run format的时候,会把代码直接格式化

四、配置husky

在上面我们已经集成好了我们代码校验工具,但是需要每次手动的去执行命令才会格式化我们的代码。如果有人没有格式化就提交了远程仓库中,那这个规范就没什么用。所以我们需要强制让开发人员按照代码规范来提交。

要做到这件事情,就需要利用husky在代码提交之前触发git hook(git在客户端的钩子),然后执行pnpm run format来自动的格式化我们的代码。

安装husky

pnpm install -D husky

执行

npx husky-init

会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行commit的时候就会执行

.husky/pre-commit文件添加如下命令:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm run format

当我们对代码进行commit操作的时候,就会执行命令,对代码进行格式化,然后再提交。

五、配置commitlint

对于我们的commit信息,也是有统一规范的,不能随便写,要让每个人都按照统一的标准来执行,我们可以利用commitlint来实现。

安装包

pnpm add @commitlint/config-conventional @commitlint/cli -D

添加配置文件,新建commitlint.config.cjs(注意是cjs),然后添加下面的代码:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  // 校验规则
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'fix',
        'docs',
        'style',
        'refactor',
        'perf',
        'test',
        'chore',
        'revert',
        'build',
      ],
    ],
    'type-case': [0],
    'type-empty': [0],
    'scope-empty': [0],
    'scope-case': [0],
    'subject-full-stop': [0, 'never'],
    'subject-case': [0, 'never'],
    'header-max-length': [0, 'always', 72],
  },
}

package.json中配置scripts命令

# 在scrips中添加下面的代码
{
"scripts": {
    "commitlint": "commitlint --config commitlint.config.cjs -e -V"
  },
}

配置结束,现在当我们填写commit信息的时候,前面就需要带着下面的subject

'feat',//新特性、新功能
'fix',//修改bug
'docs',//文档修改
'style',//代码格式修改, 注意不是 css 修改
'refactor',//代码重构
'perf',//优化相关,比如提升性能、体验
'test',//测试用例修改
'chore',//其他修改, 比如改变构建流程、或者增加依赖库、工具等
'revert',//回滚到上一个版本
'build',//编译相关的修改,例如发布版本、对项目构建或者依赖的改动

配置husky

npx husky add .husky/commit-msg 

在生成的commit-msg文件中添加下面的命令

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm commitlint

当我们 commit 提交信息时,就不能再随意写了,必须是 git commit -m ‘fix: xxx’ 符合类型的才可以,需要注意的是类型的后面需要用英文的 :,并且冒号后面是需要空一格的,这个是不能省略的

六、强制使用pnpm包管理器工具

团队开发项目的时候,需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,

导致项目出现bug问题,因此包管理器工具需要统一管理!!!

在根目录创建scritps/preinstall.js文件,添加下面的内容

if (!/pnpm/.test(process.env.npm_execpath || '')) {
  console.warn(
    `\u001b[33mThis repository must using pnpm as the package manager ` +
    ` for scripts to work properly.\u001b[39m\n`,
  )
  process.exit(1)
}

配置命令

"scripts": {
	"preinstall": "node ./scripts/preinstall.js"
}

当我们使用npm或者yarn来安装包的时候,就会报错了。原理就是在install的时候会触发preinstall(npm提供的生命周期钩子)这个文件里面的代码。

三、项目集成

3.1 集成element-plus

硅谷甄选运营平台,UI组件库采用的element-plus,因此需要集成element-plus插件!!!

官网地址:https://element-plus.gitee.io/zh-CN/

pnpm install element-plus @element-plus/icons-vue

入口文件main.ts全局安装element-plus,element-plus默认支持语言英语设置为中文

import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css'
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus, {
    locale: zhCn
})

Element Plus全局组件类型声明

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["element-plus/global"]
  }
}

配置完毕可以测试element-plus组件与图标的使用.

3.2 src别名的配置

在开发项目的时候文件与文件关系可能很复杂,因此我们需要给src文件夹配置一个别名!!!

// vite.config.ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
        }
    }
})

TypeScript 编译配置

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"] 
    }
  }
}

2024年07月26日18:30:29
只需要加这个配置就行
在这里插入图片描述

3.3 环境变量的配置

项目开发过程中,至少会经历开发环境、测试环境和生产环境(即正式环境)三个阶段。不同阶段请求的状态(如接口地址等)不尽相同,若手动切换接口地址是相当繁琐且易出错的。于是环境变量配置的需求就应运而生,我们只需做简单的配置,把环境状态切换的工作交给代码。

开发环境(development)
顾名思义,开发使用的环境,每位开发人员在自己的dev分支上干活,开发到一定程度,同事会合并代码,进行联调。

测试环境(testing)
测试同事干活的环境啦,一般会由测试同事自己来部署,然后在此环境进行测试

生产环境(production)
生产环境是指正式提供对外服务的,一般会关掉错误报告,打开错误日志。(正式提供给客户使用的环境。)

注意:一般情况下,一个环境对应一台服务器,也有的公司开发与测试环境是一台服务器!!!

项目根目录分别添加 开发、生产和测试环境的文件!

.env.development
.env.production
.env.test

文件内容

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/dev-api'
NODE_ENV = 'production'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/prod-api'
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/test-api'

配置运行命令:package.json

 "scripts": {
    "dev": "vite --open",
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
    "preview": "vite preview"
  },

通过import.meta.env获取环境变量
在这里插入图片描述

3.4 SVG图标配置

在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,

这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。

安装SVG依赖插件

pnpm install vite-plugin-svg-icons -D

vite.config.ts中配置插件

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // Specify symbolId format
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}

入口文件导入

import 'virtual:svg-icons-register'
3.4.1 svg封装为全局组件

因为项目很多模块需要使用图标,因此把它封装为全局组件!!!

在src/components目录下创建一个SvgIcon组件:代表如下

<template>
  <div>
    <svg :style="{ width: width, height: height }">
      <use :xlink:href="prefix + name" :fill="color"></use>
    </svg>
  </div>
</template>

<script setup lang="ts">
defineProps({
  //xlink:href属性值的前缀
  prefix: {
    type: String,
    default: '#icon-'
  },
  //svg矢量图的名字
  name: String,
  //svg图标的颜色
  color: {
    type: String,
    default: ""
  },
  //svg宽度
  width: {
    type: String,
    default: '16px'
  },
  //svg高度
  height: {
    type: String,
    default: '16px'
  }

})
</script>
<style scoped></style>

在src文件夹目录下创建一个index.ts文件:用于注册components文件夹内部全部全局组件!!!

import SvgIcon from './SvgIcon/index.vue';
import type { App, Component } from 'vue';
const components: { [name: string]: Component } = { SvgIcon };
export default {
    install(app: App) {
        Object.keys(components).forEach((key: string) => {
            app.component(key, components[key]);
        })
    }
}

在入口文件引入src/index.ts文件,通过app.use方法安装自定义插件

import gloablComponent from './components/index';
app.use(gloablComponent);

在这里插入图片描述
自定义插件注册所有的全局组件
在这里插入图片描述

在这里插入图片描述

3.5集成sass

pnpm i sass sass-loader -D
我们目前在组件内部已经可以使用scss样式,因为在配置styleLint工具的时候,项目当中已经安装过sass sass-loader,因此我们再组件内可以使用scss语法!!!需要加上lang=“scss”

<style scoped lang="scss"></style>

接下来我们为项目添加一些全局的样式

在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss

@import reset.scss

在入口文件引入

import '@/styles/index.scss'

但是你会发现在src/styles/index.scss全局样式文件中没有办法使用 变量 . 因此需要给项目中引入全局变量 变量.因此需要给项目中引入全局变量 变量.因此需要给项目中引入全局变量.

在style/variable.scss创建一个variable.scss文件!

在vite.config.ts文件配置如下:

export default defineConfig((config) => {
	css: {
      preprocessorOptions: {
        scss: {
          javascriptEnabled: true,
          additionalData: '@import "./src/styles/variable.scss";',
        },
      },
    },
	}
}

@import "./src/styles/variable.less";后面的;不要忘记,不然会报错!

配置完毕你会发现scss提供这些全局变量可以在组件样式中使用了!!!

3.6 mock数据

安装依赖:https://www.npmjs.com/package/vite-plugin-mock

pnpm install -D vite-plugin-mock mockjs

在 vite.config.js 配置文件启用插件。

import { UserConfigExport, ConfigEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
export default ({ command })=> {
  return {
    plugins: [
      vue(),
      viteMockServe({
        localEnabled: command === 'serve',
      }),
    ],
  }
}

在根目录创建mock文件夹:去创建我们需要mock数据与接口!!!

在mock文件夹内部创建一个user.ts文件

//用户信息数据
function createUserList() {
    return [
        {
            userId: 1,
            avatar:
                'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
            username: 'admin',
            password: '111111',
            desc: '平台管理员',
            roles: ['平台管理员'],
            buttons: ['cuser.detail'],
            routes: ['home'],
            token: 'Admin Token',
        },
        {
            userId: 2,
            avatar:
                'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
            username: 'system',
            password: '111111',
            desc: '系统管理员',
            roles: ['系统管理员'],
            buttons: ['cuser.detail', 'cuser.user'],
            routes: ['home'],
            token: 'System Token',
        },
    ]
}

export default [
    // 用户登录接口
    {
        url: '/api/user/login',//请求地址
        method: 'post',//请求方式
        response: ({ body }) => {
            //获取请求体携带过来的用户名与密码
            const { username, password } = body;
            //调用获取用户信息函数,用于判断是否有此用户
            const checkUser = createUserList().find(
                (item) => item.username === username && item.password === password,
            )
            //没有用户返回失败信息
            if (!checkUser) {
                return { code: 201, data: { message: '账号或者密码不正确' } }
            }
            //如果有返回成功信息
            const { token } = checkUser
            return { code: 200, data: { token } }
        },
    },
    // 获取用户信息
    {
        url: '/api/user/info',
        method: 'get',
        response: (request) => {
            //获取请求头携带token
            const token = request.headers.token;
            //查看用户信息是否包含有次token用户
            const checkUser = createUserList().find((item) => item.token === token)
            //没有返回失败的信息
            if (!checkUser) {
                return { code: 201, data: { message: '获取用户信息失败' } }
            }
            //如果有返回成功信息
            return { code: 200, data: {checkUser} }
        },
    },
]

安装axios

pnpm install axios

最后通过axios测试接口!!!

3.7 axios二次封装

在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候

我们经常会把axios进行二次封装。

目的:

1:使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)

2:使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)

在根目录下创建utils/request.ts

import axios from "axios";
import { ElMessage } from "element-plus";
//第一步:利用axios对象的create方法,去创建axios实例(其他的配置:基础路径、超时的时间)
let request = axios.create({
    baseURL: import.meta.env.VITE_APP_BASE_API, //基础路径上会携带/api
    timeout: 5000 //超时的时间的设置
})
//第二步:request实例添加请求与响应拦截器
request.interceptors.request.use(config => {
	//config配置对象,headers属性请求头,经常给服务器端携带公共参数
    //返回配置对象
    return config;
});
//第三步:响应拦截器
request.interceptors.response.use((response) => {
	//成功回调
    //简化数据
    return response.data;
}, (error) => {
    //失败回调:处理http网络错误的
    //定义一个变量:存储网络错误信息
    let msg = '';
    let status = error.response.status;
    switch (status) {
        case 401:
            msg = "token过期";
            break;
        case 403:
            msg = '无权访问';
            break;
        case 404:
            msg = "请求地址错误";
            break;
        case 500:
            msg = "服务器出现问题";
            break;
        default:
            msg = "无网络";

    }
    //提示错误信息
    ElMessage({
        type: 'error',
        message: msg
    })
    return Promise.reject(error);
});
export default request;

3.8 API接口统一管理

在开发项目的时候,接口可能很多需要统一管理。在src目录下去创建api文件夹去统一管理项目的接口;

比如:下面方式

//统一管理咱们项目用户相关的接口

import request from '@/utils/request'

import type {

 loginFormData,

 loginResponseData,

 userInfoReponseData,

} from './type'

//项目用户相关的请求地址

enum API {

 LOGIN_URL = '/admin/acl/index/login',

 USERINFO_URL = '/admin/acl/index/info',

 LOGOUT_URL = '/admin/acl/index/logout',

}
//登录接口
export const reqLogin = (data: loginFormData) =>
 request.post<any, loginResponseData>(API.LOGIN_URL, data)
//获取用户信息

export const reqUserInfo = () =>

 request.get<any, userInfoReponseData>(API.USERINFO_URL)

//退出登录

export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

type.ts

//定义用户相关数据的ts类型
//用户登录接口携带参数的ts类型
export interface loginFormData {
  username: string
  password: string
}

//定义全部接口返回数据都拥有ts类型
export interface ResponseData {
  code: number
  message: string
  ok: boolean
}

//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
  data: string
}

//定义获取用户信息返回数据类型
export interface userInfoReponseData extends ResponseData {
  data: {
    routes: string[]
    buttons: string[]
    roles: string[]
    name: string
    avatar: string
  }
}

四、开发

1. 登录逻辑

初版
store中的登录方法
async函数返回的是promise,promise的状态必须手动返回成功还是失败,这里如果登录成功返回ok,失败返回Promise.reject(new Error())
在这里插入图片描述

登录页面
由于上面的userLogin方法返回的是promise,这里的登录方法login要么用async函数,要么写.then来获取返回的结果,如果用普通函数只能拿到promise对象解析不出来具体返回数据
在这里插入图片描述

在这里插入图片描述

2. 递归组件生成动态菜单

layout/index.vue
在这里插入图片描述
store/modules/user.ts 只是把routes.ts里常量路由引入
在这里插入图片描述

menu/index.vue
在这里插入图片描述
routes.ts

在这里插入图片描述

上面写的不全对,重定向只和重定向有关,不用配合那段也可以,但是菜单展示不太好(不应该展示一级菜单【无用-重定向】)
在这里插入图片描述
配合menu/index.vue中那段,展示这样,直接吧首页提成一级
在这里插入图片描述

3. 完成菜单图标

注册所有图标成全局组件
在这里插入图片描述
在这里插入图片描述

4. 封装内容展示区域实现路由切换动画

在这里插入图片描述
main/index.vue
router-view上有作用域插槽,可以渲染当前组件;注意动画的类名:fade-enter-form、fade-enter-to、fade-enter-active
在这里插入图片描述

5. 面包屑动态展示

在这里插入图片描述
在面包屑点击一级路由的时候,会自动跳转到第一个子路由
在这里插入图片描述

6. 刷新按钮功能实现

在这里插入图片描述
在这里插入图片描述
pinia中
在这里插入图片描述
刷新内容展示区
在这里插入图片描述

7. 全屏按钮功能实现

在这里插入图片描述

8. 获取用户信息

在这里插入图片描述
home组件中调用获取用户信息接口
在这里插入图片描述
展示用户信息
在这里插入图片描述

9. 退出登录实现

退出登录跳转登录页携带redirect
在这里插入图片描述
登录之后根据有无redirect判断调整之前地址还是首页
在这里插入图片描述

10. 路由鉴权

permission.ts

// 路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from './router'
import setting from './setting'
// @ts-ignore
import nprogress from 'nprogress'
// 引入进度条样式
import 'nprogress/nprogress.css'
nprogress.configure({ showSpinner: false })

// 获取pinia大仓库
import pinia from './store'
// 获取用户小仓库数据
import useUserStore from './store/modules/user'
// 在这里如果不引入pinia并传入,是拿不到仓库的
const userStore = useUserStore(pinia)


router.beforeEach(async (to: any, from: any, next: any) => {
  console.log('路由鉴权', to, from)
  document.title = `${setting.title} - ${to.meta.title}`

  // 进度条开始
  nprogress.start()

  // 获取token,去判断用户登录、还是未登录
  const token = userStore.token
  // 获取用户名字
  const username = userStore.username

  // 如果用户已登录
  if (token) {
    if (to.name === 'login') {
      next({ name: 'home' })
    } else {
      if (!username) {
        // 如果用户登录了,但是没有用户信息, 获取用户信息再放行
        try {
          await userStore.userInfo()
          next()
        } catch (error) {
          // token过期获取不到用户信息了,或者,用户手动修改了token
          await userStore.userLogout()
          next({ name: 'login', query: { redirect: to.path } })
        }
      } else {
        // 如果用户登录了,有用户信息,直接放行
        next()
      }
    }
  } else {
    // 如果用户未登录
    if (to.name === 'login') {
      next()
    } else {
      next({ name: 'login', query: { redirect: to.path } })
    }
  }
})

router.afterEach(() => {
  // 进度条结束
  nprogress.done()
})

store/modules/user.ts
注意:async函数无论成功或失败都要写return

// 用户信息相关数据

import { defineStore } from 'pinia'
// 引入接口
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
// 引入数据类型
import type { loginFormData, loginResponseData  } from '@/api/user/type'
import type { UserState } from './types/type'
import { GET_TOKEN, REMOVE_TOKEN, SET_TOKEN } from '@/utils/token'
import { constantRoute } from '@/router/routes'

let useUserStore = defineStore('user', {
  state: ():UserState => {
    return {
      token: GET_TOKEN(), // 用户token
      menuRoutes: constantRoute, // 仓库存储生成菜单需要数组
      username: '', // 用户名称
      avatar: '', // 用户头像
    }
  },
  getters: {},
  actions: {
    // 登录方法
    async userLogin (data: loginFormData) {
      const result: loginResponseData = await reqLogin(data)
      console.log('登录接口返回', result)
      // 登录成功保存token
      if (result.code === 200) {
        this.token = (result.data as string)
        SET_TOKEN(this.token)
        // 保证当前async函数返回一个成功的promise
        return 'ok'
      } else {
        // 登录失败
        return Promise.reject(new Error(result.message))
      }
    },
    // 获取用户信息
    async userInfo () {
      const result = await reqUserInfo()
      console.log('用户信息接口返回', result)
      if (result.code === 200) {
        // 获取用户信息成功
        this.username = result.data.name || result.data.username
        this.avatar = result.data.avatar
        return 'ok'
      } else {
        return Promise.reject(new Error('获取用户信息失败'))
      }
    },
    // 退出登录
    async userLogout () {
      // 退出登录,其实是要发请求的,我们这里不用发请求,直接清除数据即可
      const result = await reqLogout()
      // 退出失败
      if (result.code !== 200) return Promise.reject(new Error('退出登录失败'))
      // 退出成功
      REMOVE_TOKEN()
      this.token = ''
      this.username = ''
      this.avatar = ''
      return 'ok'
    }
  }
})

export default useUserStore

11. 品牌管理模块

api/tradeMark/type.ts

export interface ResponseData {
  code: number
  message: string
  ok: boolean
}

// 已有的品牌的ts数据类型
export interface TradeMark {
  id?: number
  tmName: string
  logoUrl: string
}

// 包含全部品牌数据的ts类型
export type Records = TradeMark[]

// 获取的已有全部品牌的数据ts类型
export interface TradeMarkResponseData extends ResponseData {
  data: {
    records: Records
    total: number
    size: number
    current: number
    searchCount: boolean
    pages: number
  }
}

api/tradeMark/index.ts

// 书写品牌管理模块接口
import request from '@/utils/request'
import type { TradeMarkResponseData, TradeMark } from './type'
// 品牌管理模块接口地址
enum API {
  // 获取已有品牌接口
  TRADEMARK_URL = '/admin/product/baseTrademark/',
  // 添加品牌
  ADDTRADEMARK_URL = '/admin/product/baseTrademark/save',
  // 修改已有品牌
  UPDATETRADEMARK_URL = '/admin/product/baseTrademark/update',
  // 删除已有品牌
  DELETE_URL = '/admin/product/baseTrademark/remove/',
}
// 获取已有品牌的接口方法
// page:获取第几页 ---默认第一页
// limit:获取几个已有品牌的数据
export const reqHasTrademark = (page: number, limit: number) =>
  request.get<any, TradeMarkResponseData>(
    API.TRADEMARK_URL + `${page}/${limit}`,
  )
// 添加与修改已有品牌接口方法
export const reqAddOrUpdateTrademark = (data: TradeMark) => {
  //修改已有品牌的数据
  if (data.id) {
    return request.put<any, any>(API.UPDATETRADEMARK_URL, data)
  } else {
    //新增品牌
    return request.post<any, any>(API.ADDTRADEMARK_URL, data)
  }
}

// 删除某一个已有品牌的数据
export const reqDeleteTrademark = (id: number) =>
  request.delete<any, any>(API.DELETE_URL + id)

弹窗之后先清空表单校验,2种方法
在这里插入图片描述

12. 表格中的input自动聚焦

ref可以绑定一个函数,参数是当前元素实例,下面定义了一个数组inputArr,每次把当前input元素实例放入
在这里插入图片描述
每次新增一个input,根据索引找到对应的input调用它的focus方法,注意要在nextTick中去调用
在这里插入图片描述
点击去编辑时也是,用索引
在这里插入图片描述
pinia清空仓库所有的数据
在这里插入图片描述

13. 定义类型

api/product/spu/type.ts

//服务器全部接口返回的数据类型
export interface ResponseData {
  code: number
  message: string
  ok: boolean
}

//SPU数据的ts类型:需要修改
export interface SpuData {
  category3Id: string | number
  id?: number
  spuName: string
  tmId: number | string
  description: string
  spuImageList: null | SpuImg[]
  spuSaleAttrList: null | SaleAttr[]
}
//数组:元素都是已有SPU数据类型
export type Records = SpuData[]
//定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponseData extends ResponseData {
  data: {
    records: Records
    total: number
    size: number
    current: number
    searchCount: boolean
    pages: number
  }
}

//品牌数据的TS类型
export interface Trademark {
  id: number
  tmName: string
  logoUrl: string
}
//品牌接口返回的数据ts类型
export interface AllTradeMark extends ResponseData {
  data: Trademark[]
}

//商品图片的ts类型
export interface SpuImg {
  id?: number
  imgName?: string
  imgUrl?: string
  createTime?: string
  updateTime?: string
  spuId?: number
  name?: string
  url?: string
}
//已有的SPU的照片墙数据的类型
export interface SpuHasImg extends ResponseData {
  data: SpuImg[]
}

//已有的销售属性值对象ts类型
export interface SaleAttrValue {
  id?: number
  createTime?: null
  updateTime?: null
  spuId?: number
  baseSaleAttrId: number | string
  saleAttrValueName: string
  saleAttrName?: string
  isChecked?: null
}
//存储已有的销售属性值数组类型
export type SpuSaleAttrValueList = SaleAttrValue[]

//销售属性对象ts类型
export interface SaleAttr {
  id?: number
  createTime?: null
  updateTime?: null
  spuId?: number
  baseSaleAttrId: number | string
  saleAttrName: string
  spuSaleAttrValueList: SpuSaleAttrValueList
  flag?: boolean
  saleAttrValue?: string
}
//SPU已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponseData {
  data: SaleAttr[]
}

//已有的全部SPU的返回数据ts类型
export interface HasSaleAttr {
  id: number
  name: string
}

export interface HasSaleAttrResponseData extends ResponseData {
  data: HasSaleAttr[]
}

export interface Attr {
  attrId: number | string //平台属性的ID
  valueId: number | string //属性值的ID
}

export interface saleArr {
  saleAttrId: number | string //属性ID
  saleAttrValueId: number | string //属性值的ID
}
export interface SkuData {
  category3Id: string | number //三级分类的ID
  spuId: string | number //已有的SPU的ID
  tmId: string | number //SPU品牌的ID
  skuName: string //sku名字
  price: string | number //sku价格
  weight: string | number //sku重量
  skuDesc: string //sku的描述
  skuAttrValueList?: Attr[]
  skuSaleAttrValueList?: saleArr[]
  skuDefaultImg: string //sku图片地址
}

//获取SKU数据接口的ts类型
export interface SkuInfoData extends ResponseData {
  data: SkuData[]
}

src/api/product/spu/index.ts

//SPU管理模块的接口
import request from '@/utils/request'
import type {
  SkuInfoData,
  SkuData,
  SpuData,
  HasSpuResponseData,
  AllTradeMark,
  SpuHasImg,
  SaleAttrResponseData,
  HasSaleAttrResponseData,
} from './type'
enum API {
  //获取已有的SPU的数据
  HASSPU_URL = '/admin/product/',
  //获取全部品牌的数据
  ALLTRADEMARK_URL = '/admin/product/baseTrademark/getTrademarkList',
  //获取某个SPU下的全部的售卖商品的图片数据
  IMAGE_URL = '/admin/product/spuImageList/',
  //获取某一个SPU下全部的已有的销售属性接口地址
  SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/',
  //获取整个项目全部的销售属性[颜色、版本、尺码]
  ALLSALEATTR_URL = '/admin/product/baseSaleAttrList',
  //追加一个新的SPU
  ADDSPU_URL = '/admin/product/saveSpuInfo',
  //更新已有的SPU
  UPDATESPU_URL = '/admin/product/updateSpuInfo',
  //追加一个新增的SKU地址
  ADDSKU_URL = '/admin/product/saveSkuInfo',
  //查看某一个已有的SPU下全部售卖的商品
  SKUINFO_URL = '/admin/product/findBySpuId/',
  //删除已有的SPU
  REMOVESPU_URL = '/admin/product/deleteSpu/',
}
//获取某一个三级分类下已有的SPU数据
export const reqHasSpu = (
  page: number,
  limit: number,
  category3Id: string | number,
) =>
  request.get<any, HasSpuResponseData>(
    API.HASSPU_URL + `${page}/${limit}?category3Id=${category3Id}`,
  )
//获取全部的SPU的品牌的数据
export const reqAllTradeMark = () =>
  request.get<any, AllTradeMark>(API.ALLTRADEMARK_URL)
//获取某一个已有的SPU下全部商品的图片地址
export const reqSpuImageList = (spuId: number) =>
  request.get<any, SpuHasImg>(API.IMAGE_URL + spuId)
//获取某一个已有的SPU拥有多少个销售属性
export const reqSpuHasSaleAttr = (spuId: number) =>
  request.get<any, SaleAttrResponseData>(API.SPUHASSALEATTR_URL + spuId)
//获取全部的销售属性
export const reqAllSaleAttr = () =>
  request.get<any, HasSaleAttrResponseData>(API.ALLSALEATTR_URL)
//添加一个新的SPU的
//更新已有的SPU接口
//data:即为新增的SPU|或者已有的SPU对象
export const reqAddOrUpdateSpu = (data: SpuData) => {
  //如果SPU对象拥有ID,更新已有的SPU
  if (data.id) {
    return request.post<any, any>(API.UPDATESPU_URL, data)
  } else {
    return request.post<any, any>(API.ADDSPU_URL, data)
  }
}
//添加SKU的请求方法
export const reqAddSku = (data: SkuData) =>
  request.post<any, any>(API.ADDSKU_URL, data)

//获取SKU数据
export const reqSkuList = (spuId: number | string) =>
  request.get<any, SkuInfoData>(API.SKUINFO_URL + spuId)

//删除已有的SPU
export const reqRemoveSpu = (spuId: number | string) =>
  request.delete<any, any>(API.REMOVESPU_URL + spuId)

14. 暗黑模式&主题颜色

暗黑模式main.ts引入
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
暗黑模式是给根节点html增加类

在这里插入图片描述

15. 数据大屏解决方案 scale缩放

把box(数据展示区域)设置定位left 50%,top: 50%,修改transform-origin: left top;,左上角为转换中心;页面初始化及缩放的时候,设置box.style.transform = scale(${getScale()}) translate(-50%,-50%),把它拉回页面中心,并设置缩放,缩放比例:设备宽高当中谁小取谁,以最小的缩放显示并且能保证缩放比例(window.innerWidth为设备宽高,默认参数w h 是设计稿尺寸)
优点:设计稿多少尺寸px,开发时直接写多少尺寸(px)
缺点:有留白

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .container {
            width: 100vw;
            height: 100vh;
            background: url(./bg.png) no-repeat;
            background-size: cover;
        }

        .box {
            position: fixed;
            width: 1920px;
            height: 1080px;
            background: red;
            transform-origin: left top;
            left: 50%;
            top: 50%;
        }

        .top {
            width: 100px;
            height: 100px;
            background: hotpink;
            margin-left: 50px;
        }

        .bottom {
            width: 100px;
            height: 100px;
            background: skyblue;
            margin-left: 50px;
            margin-top: 100px;
        }
    </style>
</head>

<body>
    <div class="container">
        <!-- 数据展示的区域 -->
        <div class="box">
            <div class="top">我是祖国的</div>
            <div class="bottom">老花骨朵</div>
        </div>
    </div>
</body>

</html>
<script>
    //控制数据大屏放大与缩小
    let box = document.querySelector('.box');
    box.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
    //计算缩放的比例啦
    function getScale(w = 1920, h = 1080) {
        const ww = window.innerWidth / w;
        const wh = window.innerHeight / h;
        const ratio = ww < wh ? ww : wh
        console.log(ratio)
        return ww < wh ? ww : wh;
        //ww<wh情况: 1920/1920(ww)   1080/1080(wh)
        //ww>wh情况:1920/1920(ww)   1080/1080(wh)
    }

    window.onresize = () => {
        box.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
    }

</script>

16. 水球图

echarts:国内镜像网站
https://www.isqqw.com/echarts-doc/zh/option.html#title
http://datav.aliyun.com/portal/school/atlas/area_selector

https://www.npmjs.com/package/echarts 在npm echarts里 Extension部分有水球图

效果
在这里插入图片描述

<template>
    <div class="box">
        <div class="top">
            <p class="title">实时游客统计</p>
            <p class="bg"></p>
            <p class="right">可预约总量<span>99999</span></p>
        </div>
        <div class="number">
            <span v-for="(item, index) in people" :key="index">{{ item }}</span>
        </div>
        <!-- 水球图 -->
        <!-- 盒子将来echarts展示图形图标的节点 -->
        <div class="charts" ref="charts">123</div>
    </div>
</template>

<script setup lang="ts">
// echarts水球图拓展插件
import 'echarts-liquidfill'
import * as echarts from 'echarts';
import { ref, onMounted } from 'vue';
let people = ref('215908人');


//获取节点
let charts = ref();
onMounted(() => {
    //获取echarts类的实例
    let mycharts = echarts.init(charts.value);
    //设置实例的配置项
    mycharts.setOption({
        //标题组件
        title: {
            text: '水球图'
        },
        //x|y轴组件
        xAxis: {},
        yAxis: {},
        //系列:决定你展示什么样的图形图标
        series: {
            type: 'liquidFill',//系列
            data: [0.6, 0.4, 0.2],//展示的数据
            waveAnimation: true,//动画
            animationDuration: 3,
            animationDurationUpdate: 0,
            radius: '100%',//半径
            outline: {//外层边框颜色设置
                show: true,
                borderDistance: 8,
                itemStyle: {
                    color: 'skyblue',
                    borderColor: '#294D99',
                    borderWidth: 8,
                    shadowBlur: 20,
                    shadowColor: 'rgba(0, 0, 0, 0.25)'
                }
            },
        },
        //布局组件
        grid: {
            left: 0,
            right: 0,
            top: 0,
            bottom: 0
        }

    })
})
</script>

17. 横向单个柱状图

在这里插入图片描述

<template>
  <div class="box1">
    <div class="title">
      <p>男女比例</p>
      <img src="../../images/dataScreen-title.png" alt="">
    </div>
    <div class="sex">
      <div class="man">
        <img src="../../images/man.png" alt="">
      </div>
      <div class="women">
        <img src="../../images/woman.png" alt="">
      </div>

    </div>
    <div class="rate">
      <p>男士58%</p>
      <p>女士42%</p>
    </div>
    <div class="charts" ref='charts'></div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
// 获取图形图标的DOM节点
let charts = ref();
onMounted(() => {
  // 初始化echarts实例
  let mycharts = echarts.init(charts.value);
  // 设置配置项
  mycharts.setOption({
    // 组件标题
    title: {
      text: '男女比例',//主标题
      textStyle: {//主标题颜色
        color: 'red'
      },
      left: '40%'
    },
    // x|y
    xAxis: {
      show: false,
      min: 0,
      max: 100
    },
    yAxis: {
      show: false,
      type: 'category'
    },
    series: [
      {
        type: 'bar',
        data: [30],
        barWidth: 20,
        z: 100,
        itemStyle: {
          color: 'skyblue',
          borderRadius: 20
        }
      }
      ,
      {
        type: 'bar',
        data: [100], // 100 - 30
        barWidth: 20,
        // 调整女士柱条位置
        barGap: '-100%',
        z: 50,
        itemStyle: {
          color: 'pink',
          borderRadius: 20
        }
      },
      // {
      //   type: 'bar',
      //   data: [100],
      //   barWidth: 20,
      //   // 调整非男非女柱条位置
      //   barGap: '-100%',
      //   z: 10,
      //   itemStyle: {
      //     color: 'red',
      //     borderRadius: 20
      //   }
      // }
    ],
    grid: {
      left: 0,
      top: 0,
      right: 0,
      bottom: 0
    }
  });
})

18. 饼状图–环形饼状图

在这里插入图片描述

<template>
    <div class="box2">
        <div class="title">
            <p>年龄比例</p>
            <img src="../../images/dataScreen-title.png" alt="">
        </div>
        <!-- 图形图标的容器 -->
        <div class="charts" ref="charts"></div>
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
// 引入echarts
import * as echarts from 'echarts';
let charts = ref();
// 组件挂载完毕初始化图形图标
onMounted(() => {
    let mychart = echarts.init(charts.value);
    // 设置配置项
    let option = {
        tooltip: {
            trigger: 'item'
        },
        legend: {
            right: 30,
            top: 40,
            orient: 'vertical',//图例组件方向的设置
            textStyle: {
                color: 'white',
                fontSize: 14
            }
        },
        series: [
            {
                name: 'Access From',
                type: 'pie',
                radius: ['40%', '70%'],
                avoidLabelOverlap: false,
                itemStyle: {
                    borderRadius: 10,
                    borderColor: '#fff',
                    borderWidth: 2
                },
                label: {
                    show: true,
                    position: 'inside',
                    color:'white'
                },

                labelLine: {
                    show: false
                },
                data: [
                    { value: 1048, name: '军事' },
                    { value: 735, name: '新闻' },
                    { value: 580, name: '直播' },
                    { value: 484, name: '娱乐' },
                    { value: 300, name: '财经' }
                ]
            }
        ],
        // 调整图形图标的位置
        grid: {
            left: 0,
            top: 0,
            right: 0,
            bottom: 0
        }
    };
    mychart.setOption(option);
});
</script>

22. 菜单权限完成(与接口返回路由匹配)

接口返回的是路由的name属性
在这里插入图片描述
store/modules/user.ts
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
permission.ts

// 路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from './router'
import setting from './setting'
// @ts-ignore
import nprogress from 'nprogress'
// 引入进度条样式
import 'nprogress/nprogress.css'
nprogress.configure({ showSpinner: false })

// 获取pinia大仓库
import pinia from './store'
// 获取用户小仓库数据
import useUserStore from './store/modules/user'
// 在这里如果不引入pinia并传入,是拿不到仓库的
const userStore = useUserStore(pinia)


router.beforeEach(async (to: any, from: any, next: any) => {
  console.log('路由鉴权', to, from)
  document.title = `${setting.title} - ${to.meta.title}`

  // 进度条开始
  nprogress.start()

  // 获取token,去判断用户登录、还是未登录
  const token = userStore.token
  // 获取用户名字
  const username = userStore.username
  // 如果用户已登录
  if (token) {
    if (to.name === 'login') {
      next({ name: 'home' })
    } else {
      if (!username) {
        // 如果用户登录了,但是没有用户信息, 获取用户信息再放行
        try {
          await userStore.userInfo()
          // 原来写的next(),改成next({...to}),原因是刷新页面的时候如果是异步路由,有可能获取到用户信息后异步路由还没有加载完毕,出现空白的效果
          next({ ...to })
        } catch (error) {
          console.log(444444444444444444)
          // token过期获取不到用户信息了,或者,用户手动修改了token
          await userStore.userLogout()
          next({ name: 'login', query: { redirect: to.path } })
        }
      } else {
        // 如果用户登录了,有用户信息,直接放行
        next()
      }
    }
  } else {
    // 如果用户未登录
    if (to.name === 'login') {
      next()
    } else {
      next({ name: 'login', query: { redirect: to.path } })
    }
  }
})

router.afterEach(() => {
  // 进度条结束
  nprogress.done()
})

未动
在这里插入图片描述

23. 按钮权限、自定义指令

定义指令
在这里插入图片描述
main.ts引入在这里插入图片描述
使用
在这里插入图片描述
store/modules/user.ts
在这里插入图片描述
用户信息接口返回示例 buttons
在这里插入图片描述

pnpm run build
报错,修改:修改缩进、双引号改单引号
在这里插入图片描述

在这里插入图片描述

部署
ng配置
在这里插入图片描述
重启ng服务
在这里插入图片描述
访问报403,再次修改nginx.conf,这里改成root
在这里插入图片描述

五、项目的资源地址

老师代码仓库地址:https://gitee.com/jch1011/vue3_admin_template-bj1.git

项目在线文档:

服务器域名:http://sph-api.atguigu.cn

swagger文档:

http://139.198.104.58:8209/swagger-ui.html

http://139.198.104.58:8212/swagger-ui.html#/

echarts:国内镜像网站

https://www.isqqw.com/echarts-doc/zh/option.html#title

http://datav.aliyun.com/portal/school/atlas/area_selector

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值