02_深入浅出vite(二)--vue3全家桶+ts构建后管系统

news2024/11/24 14:38:18

安装基础包

npm create vite@latest
# 这里选择的是Vue+Typescript的组合
cd vue-admin
npm install

# 先安装基础包
npm install vue-router@4
npm i pinia
npm i axios
npm install sass --save-dev
npm install element-plus --save
npm install @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
npm i eslint -D

# 提交规范
npm i lint-staged husky  --save-dev
npm install @commitlint/cli @commitlint/config-conventional -D

代码规范

npm init @eslint/config

接下来会有一堆提示,选择如下:

Need to install the following packages:
  @eslint/create-config
Ok to proceed? (y)
√ How would you like to use ESLint? · style       
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:

eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.50.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-plugin-vue@latest, eslint-config-standard-with-typescript@latest, @typescript-eslint/eslint-plugin@^5.50.0, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, typescript@*

在项目中就会生成一个.eslintrc.cjs文件,接下来配置一下脚本验证一下:

"lint": "eslint src/**/*.{js,jsx,vue,ts,tsx} --fix"

然而运行的时候报错了,由于我当前的"typescript": "^5.1.3",@typescript-eslint/typescript-estree支持的ts版本范围为:=3.3.1 <5.1.0,所以我得降级一下:typescript@5.0.4,在配置eslint路上出现了很多问题,直接提供解决方案:

首先是修改.eslintrc.cjs:

module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'standard-with-typescript'
  ],
  parser: "vue-eslint-parser",
  overrides: [
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: ["./tsconfig.json"],
    parser: "@typescript-eslint/parser",
    extraFileExtensions: ['.vue']
  },
  plugins: [
    'vue'
  ],
  rules: {
    'space-before-function-paren': [2, {
      anonymous: 'always',
      named: 'never',
      asyncArrow: 'always'
    }],
    'vue/multi-word-component-names': 0,
    "space-before-function-paren": 0,
    "@typescript-eslint/consistent-type-assertions": 0,
    "@typescript-eslint/ban-types": [
      "error",
      {
        "extendDefaults": true,
        "types": {
          "{}": false
        }
      }
    ]
  }
}

vite-env.d.ts加上一行注释,忽略检查:

// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />

关于eslint配置中遇到的问题,可以参考这个大佬写的,更详细些:Eslint:vue3项目添加eslint(standard规则)

commit规范

git init

package.json中新增如下代码,利用它来调用 eslint 和 stylelint 去检查暂存区内的代码

"lint-staged": {
    "*.{vue,js}": [
      "npm run lint"
    ]
  }

执行:

npm pkg set scripts.postinstall="husky install"
# 等同于执行npm i,执行过程中会生成.husky文件夹
npm run postinstall

npx husky add .husky/pre-commit "npm lint"
git add .husky/pre-commit

这样我们执行git commit的时候就会自动执行npm lint

很尴尬,在跑的过程中,报错了node不是内部或外部命令。node -v是木有问题的,大抵是nvm这个工具的问题,所以后面就换了volta来做node的版本控制。

npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"

新建commitlint.config.cjs

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat', // 新增功能
      'update', // 更新功能
      'ui', // 样式改动
      'fix', // 修复功能bug
      'merge', // 合并分支
      'refactor', // 重构功能
      'perf', // 性能优化
      'revert', // 回退提交
      'style', // 不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等)
      'build', // 修改项目构建工具(例如 glup,webpack,rollup 的配置等)的提交
      'docs', // 文档新增、改动
      'test', // 增加测试、修改测试
      'chore' // 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
    ]],
    'scope-empty': [0],
    // 'scope-empty': [2, 'never'], 作用域不为空
    'scope-case': [0],
    'subject-full-stop': [0],
    'subject-case': [0]
  }
}

修改tsconfig.json

"include": [
  //...
  "commitlint.config.cjs"
 ],

修改.eslintrc.cjs

project: ["./tsconfig.json", "./commitlint.config.cjs"],
git add .
# 失败
git commit -m "commit校验"
# 成功
git commit -m "feat: commit校验"

设置路径别名

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve('src')
    },
    // 顺便把可以省略的后缀配置一下,在vite中不支持省略.vue
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  }
})

修改tsconfig.json,新增:

"compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },

重置样式

assets文件夹下新建styles/reset.css

/**
 * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
 * http://cssreset.com
 */
 
 html, body, div, span, applet, object, iframe,
 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
 a, abbr, acronym, address, big, cite, code,
 del, dfn, em, img, ins, kbd, q, s, samp,
 small, strike, strong, sub, sup, tt, var,
 b, u, i, center,
 dl, dt, dd, ol, ul, li,
 fieldset, form, label, legend,
 table, caption, tbody, tfoot, thead, tr, th, td,
 article, aside, canvas, details, embed, 
 figure, figcaption, footer, header, hgroup, 
 menu, nav, output, ruby, section, summary,
 time, mark, audio, video{
   margin: 0;
   padding: 0;
   border: 0;
   font-size: 100%;
   font: inherit;
   font-weight: normal;
   vertical-align: baseline;
 }
 /* HTML5 display-role reset for older browsers */
 article, aside, details, figcaption, figure, 
 footer, header, hgroup, menu, nav, section{
   display: block;
 }
 ol, ul, li{
   list-style: none;
 }
 blockquote, q{
   quotes: none;
 }
 blockquote:before, blockquote:after,
 q:before, q:after{
   content: '';
   content: none;
 }
 table{
   border-collapse: collapse;
   border-spacing: 0;
 }
  
 /* custom */
 a{
   color: #7e8c8d;
   text-decoration: none;
   backface-visibility: hidden;
   -webkit-backface-visibility: hidden;
 }
 ::-webkit-scrollbar{
   width: 5px;
   height: 5px;
 }
 ::-webkit-scrollbar-track-piece{
   background-color: rgba(0, 0, 0, 0.2);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:vertical{
   height: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:horizontal{
   width: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 html, body{
   width: 100%;
   font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", "微软雅黑", sans-serif;
 }
 body{
   line-height: 1;
   -webkit-text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 }
 html{
   overflow-y: scroll;
 }
  
 /*清除浮动*/
 .clearfix:before,
 .clearfix:after{
   content: " ";
   display: inline-block;
   height: 0;
   clear: both;
   visibility: hidden;
 }
 .clearfix{
   *zoom: 1;
 }
  
 /*隐藏*/
 .dn{
   display: none;
 }
 

使用Scss

Vite 提供了对 .scss, .sass, .less, .styl.stylus 文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖。

一般我们会在项目中定义一些主题色:

// variable.scss
$font-color-gray:rgb(147,147,147);

或者是一些封装好的集合样式:

// mixins.scss
@mixin line-clamp($lines) {
  word-break: break-all;
  display: -webkit-box;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: $lines;
  -webkit-box-orient: vertical;
}

@mixin ellipsis() {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}


然后我们在vite.config.js中配置:

css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }

删除目录中的style.css,新建styles/common.scss

// common.scss
@import url('./reset.css');

然后在main.ts中引入:

import '@/assets/styles/common.scss'

这样全局样式就初始化了,接下来测试一下variable.scssmixins.scss是否起作用,修改HelloWorld.vue查看是否为灰色且两行省略:

<script setup lang="ts">
</script>

<template>
  <div class="box">没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。</div>
</template>

<style scoped lang="scss">
.box {
  color: $font-color-gray;
  height: 40px;
  line-height: 20px;
  width: 200px;
  @include line-clamp(2);
}
</style>

配置路由

现在要来配置路由,希望有这样的结果:

- 登录页
- 带菜单栏的框架
  - 主页
  - 人员管理
    - 客户管理
    - 员工管理
- 404

所以新建以下文件:

在这里插入图片描述

在这个过程中我们会遇到几个问题:

  • 在编写路由的时候我们引入组件,必须有.vue后缀;
  • import xxx from '@/xxx'时会报错,是因为你没有在上面提到的那样在tsconfg.json中设置baseUrlpaths值。

由于layout文件中要嵌套子路由,所以layout中要加入router-view:

<template>
  <div>布局</div>
  <router-view></router-view>
</template>

其他的文件只要写成这样就行:

<template>
  <p>主页</p>
</template>

新建router文件夹:

- router
  -hooks # 后期做登录校验和鉴权用的
  - routes
    - index.ts # 总输出文件
    - others.ts # 不需要layout这一层的路由均可以放在这里
    - person.ts # 人员管理模块
  - index.ts   # 总输出文件

接下里是各个文件的内容:

// person.ts
export default [
  {
    path: '/person',
    name: 'Person',
    meta: { title: '人员管理' },
    redirect: '/person/customer',
    children: [
      {
        path: '/person/customer',
        name: 'PersonCustomer',
        meta: { title: '客户管理' },
        component: () => import('@/views/person/customer/index.vue')
      },
      {
        path: '/person/staff',
        name: 'PersonStaff',
        meta: { title: '员工管理' },
        component: () => import('@/views/person/staff/index.vue')
      }
    ]
  }
];

// others.ts
export default [
  {
    path: '/login',
    name: 'Login',
    meta: { title: '登录' },
    component: () => import('@/views/login/index.vue')
  }
];

// router/routes/index.ts
import Layout from '@/views/layout/index.vue';
import personRoutes from './person';
import otherRoutes from './others';

export default [
  {
    path: '/',
    name: 'Layout',
    component: Layout,
    children: [
      {
        path: '/',
        name: 'Index',
        meta: { title: '主页' },
        component: () => import('@/views/index/index.vue')
      },

      ...personRoutes,
    ]
  },
  ...otherRoutes,
  {
    path: '/404',
    name: 'NotFound',
    meta: { title: '404' },
    component: () => import('@/views/404/index.vue')
  },
  {
    path: "/:pathMatch(.*)",
    redirect: "/404",
    name:'ErrorPage',
    meta: { title: '' },
  }
];

先抛开hooks文件夹,简单的写一下index.ts

// router/index.ts
import routes from "./routes";
export default routes;

新建一个src/plugins/index.ts,之前我们注册内容的时候都是直接放在main.ts中,不太容易维护,所以以后统一在这里挂载:

// src/plugins/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import routes from '@/router/index';

export default (app: any) => {
  // 注册路由
  const router = createRouter({
    history: createWebHashHistory(),
    routes
  })

  app.use(router);
}

不要忘了修改App.vue:

<template>
  <router-view></router-view>
</template>
import { createApp } from 'vue'
import '@/assets/styles/common.scss'
import App from './App.vue'
import installPlugins from '@/plugins';

const app = createApp(App);
installPlugins(app);
app.mount('#app')

这样就可以测试了:

http://127.0.0.1:5173/#/
http://127.0.0.1:5173/#/login
http://127.0.0.1:5173/#/person
http://127.0.0.1:5173/#/person/customer
http://127.0.0.1:5173/#/person/staff

如果在配置过程中发现报错:找不到模块“xxx.vue”或其相应的类型声明,则在vite-env.d.ts中新增:

declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const vueComponent: DefineComponent<{}, {}, any>;
  export default vueComponent;
} 

使用element plus

如果您使用 Volar,请在 tsconfig.json 中通过 compilerOptions.type 指定全局组件类型。

// tsconfig.json
{
  "compilerOptions": {
    // ...
    // 然而这个配置在后期打包的时候报错了...
    "types": ["element-plus/global"]
  }
}

这里采用了按需引入的方式,如果对体积不追求的,可以采用完整引入:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
  plugins: [
    vue(),
    // 新增
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    // 新增
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    alias: {
      '@': resolve('./src')
    },
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }
})


element plus中日期等组件默认是英文,所以我们把组件改为中文:

在这里插入图片描述

修改App.vue

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

这样就会出现中文了。

在这里插入图片描述

关于引入的两个插件,这里解释一下:

  • unplugin-vue-components用于自动识别Vue模板中使用的组件,自动按需导入和注册;
  • unplugin-auto-import可以在vite、webpack等环境下自动按需导入配置库常用的API,如Vueref,不需要手动import,所以我们可以配置一下,并删除一些API的引入:
export default defineConfig({
  plugins: [
    // ...
    AutoImport({
      imports: [
        'vue',
        'vue-router',
        'pinia'
      ],
      eslintrc: {
        enabled: true,
        filepath: './.eslintrc-auto-import.json',
        globalsPropValue: true
      },
      resolvers: [ElementPlusResolver()]
    }),
    // ...
  ],
})

保存生效后,auto-imports.d.ts 会自动填充内容,并且会在项目根目录生成 .eslintrc-auto-import.json eslint 全局变量配置。

然后修改tsconfg.json.eslintrc.cjs

// tsconfg.json
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "commitlint.config.cjs", "auto-imports.d.ts"],

// .eslintrc.cjs
project: ["./tsconfig.json", "./commitlint.config.cjs", './.eslintrc-auto-import.json'],

忽略 auto-imports.d.ts ESLint 校验

# .eslintignore
auto-imports.d.ts

这里需要注意一下:

  1. 不是全部 API,例如 Vue Router 的 createRouter 就不会导入。具体可以自动导入的 API 参考 unplugin-auto-import/src/presets
  2. 生成 .eslintrc-auto-import.json 文件后如不需要增加配置建议将 enabled: true 设置为 false,否则每次都会生成这个文件。

配置完可以删除页面中的一些引用,发现是没有问题的。

<script lang='ts' setup>
// import { storeToRefs } from 'pinia'
// import { useRouter } from 'vue-router'
// ...
</script>

测试一下组件:

<template>
  <p><el-button>测试</el-button></p>
</template>

这样页面上就会显示按钮了。

自动按需引入的原理是通过识别<template>中使用的组件自动导入,那类似ElMessage 这类直接在 JS 中调用方法的组件,插件并不会识别并完成自动导入,所以还是需要自己手动导入一下(建议按需引入的方式,仍然引入完整的样式文件,避免这类边界问题):

修改vite-env.d.ts,不然在ts中引入element plus会报错:

declare module "element-plus";

plugins/element-plus.ts中小试一下:

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

const options = {
  size: 'small',
  zIndex: 3000
}

const components = [
  ElLoading,
  ElMessage,
  ElMessageBox, 
  ElNotification
]

export default function install (app: any): void {
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }

  components.forEach((component) => {
    app.use(component, options)
  })
}


// plugins/index.ts
export default (app: any) => {
  // ...

  // 注册element-plus
  installElementPlus(app);
}

测试一下:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const onTest = () => {
  ElLoading.service({ fullscreen: true });
}
</script>

globalProperties

按照以前的习惯,loading的调用肯定不用上面的方式,而是挂载在全局Vue.prototype上,然而在这个项目中,我们用的是按需引入,且在Vue3中,写法变了,你可能想这么写:

app.config.globalProperties.$loading = ElLoading;
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$msgBox = ElMessageBox;
app.config.globalProperties.$notification = ElNotification;

然后再使用的过程中:

<script setup lang="ts">
const instance = getCurrentInstance()

onMounted(() => {
  instance.proxy.$message.success('setup - getCurrentInstance() 成功使用')
  // 也可以使用 appContext
  console.log(instance.appContext.config.globalProperties.$message === instance.proxy.$message) // true
})
</script>

但是查阅了官方文档,并没有getCurrentInstance该方法,大概是不符合规范吧。所以全局方法的注入,我采用了provide/inject

App.vue

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

provide('$loading', ElLoading)
provide('$message', ElMessage)
provide('$messagebox', ElMessageBox)
provide('$notification', ElNotification)
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

修改element-plus.ts

import { App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

// const options = {
//   size: 'small',
//   zIndex: 3000
// }

// const components = [
//   ElLoading,
//   ElMessage,
//   ElMessageBox, 
//   ElNotification
// ]

export default function install (app: App): void {
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }

  // components.forEach((component) => {
  //   app.use(component, options)
  // })
}

新增src/types/global.d.ts

import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

export interface ComponentCustomProperties {
  $message: typeof ElMessage
  $msgBox: typeof ElMessageBox
  $loading: typeof ElLoading
  $notification: typeof ElNotification
}


测试:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const $loading = inject('$loading') as any 
const onTest = () => {
  $loading.service({
    lock: true,
    text: 'Loading',
  })
}
</script>

复习一些常见的ts写法

开发服务器和打包器将只会对ts文件执行语法转义,而不会执行任何类型检查,保证了vite开发服务器在使用ts时也能保持飞快的速度。

下面是一些常见的例子:

<!-- 对props的类型声明和默认值 -->
<script setup lang="ts">
interface Props {
  msg?: string
  labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
   msg: 'hello',
   labels: () => ['one', 'two']
})
</script>


<!-- 另一种方式 -->
<script setup lang="ts">
interface Book {
  title?: string
  author: string
}
const props = defineProps({
  book: Object as PropType<Book>
})
</script>


<!-- 对emits进行声明 -->
<script setup lang="ts">
// 1.运行时
const emit = defineEmits(['change', 'update'])

// 2.基于类型
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>


<!-- 为computed指定返回类型 --->
<script setup lang="ts">
const double = computed<number>(() => { /** return number */ })
</script>

<!-- 为函数参数标注类型 -->
<script setup lang="ts">
const onClick = (e: Event) => {
  console.log((event.target as HTMLInputElement).value)
}
</script>

<!-- project和inject 然而我很少用到 -->
<script setup lang="ts">
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>
provide(key, 'foo')

const foo = inject<string>('foo', 'bar')
</script>

<!-- 为模板引用标注类型 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>


<!-- 为组件模板引用标注类型 -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
  modal.value?.open()
}
</script>

登录静态页面

在登录页面中会使用到el-input组件,一般对于这种表单组件,后管使用的频率还是很高的,所以倾向于进行二次封装再使用,一般情况下会优化它的前后空格问题,并将它与textarea拆解出来:

<script lang='ts' setup>
import { computed } from 'vue';
const emits = defineEmits<{
  (e: 'update:value', value: string): void;
  (e: 'blur'): void;
  (e: 'focus'): void;
  (e: 'change', value: string): void;
  (e: 'clear'): void;
}>()

const props = defineProps({
  type: {
    type: String,
    default: 'string'
  },
  value: {
    type: [String, Number],
    default: '',
    required: true
  },
  maxlength: [String, Number],
  minlength: [String, Number],
  placeholder: {
    type: String,
    default: '请输入',
  },
  clearable: {
    type: Boolean,
    default: true
  },
  showPassword: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  },
  prefixIcon: {
    type: String,
    default: ''
  },
  suffixIcon: {
    type: String,
    default: ''
  },
  inputStyle: [String, Object],
  showWordLimit: {
    type: Boolean,
    default: true
  },
  rows: Number,
  autosize: [Boolean, Object]
})

const input = computed({
  get(){
    console.log(props.value)
    return props.value;
  },

  set(val: any){
    if(typeof val === 'string') {
      val = val ? val.trim() : val;
    }
    emits('update:value', val);
  }
})

const onFocus = () => {
  emits('focus');
}

const onBlur = () => {
  emits('blur');
}

const onClear = () => {
  emits('clear');
}

const onChange = (val: string) => {
  emits('change', val);
}

</script>

<template>
  <el-input
    v-if="type === 'textarea'"
    v-model="input"
    :rows="rows"
    type="textarea"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :show-word-limit="showWordLimit"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :autosize="autosize"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @change="onChange"
  />
  <el-input 
    v-else
    v-model="input" 
    :type="type"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :clearable="clearable"
    :showPassword="showPassword"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @clear="onClear"
    @change="onChange" />
    
</template>

<style scoped lang='scss'>

</style>

plugins/components.ts下全局注册:

import type { Component } from 'vue'
import ArInput from '@/components/form/input/index.vue';

const componentObj: {[propName: string]: Component} = {
  ArInput
};

export default function install(app: any) {
  Object.keys(componentObj).forEach((key) => {
    app.component(key, componentObj[key])
  })
}

记得在plugins/index.ts中加入:

import installComponents from './components'

export default (app: any) => { 
  // ...

  // 注册自定义组件
  installComponents(app);
}

login.vue的源码:

<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus'
import { ref, reactive } from 'vue'


const formRef = ref<FormInstance>();
const form = reactive({
  name: '',
  password: ''
})
const rules = ref<FormRules>({
  name: [
    { required: true, message: '请输入账号', trigger: 'blur' },
    { min: 8, max: 12, message: '账号长度为8-12', trigger: 'blur' },
  ],
  password: [
  { required: true, message: '请输入密码', trigger: 'blur' },
  ]
})

const onSubmit = async() => {
  await formRef.value.validate((valid, fields) => {
    if (valid) {
      console.log('submit!')
    } else {
      console.log('error submit!', fields)
    }
  })
}

</script>

<template>
  <div class="login">
    <div class="login-inner">
      <el-form ref="formRef" :model="form" :rules="rules">
        <el-form-item label="账号" prop="name" required>
          <ArInput v-model:value="form.name" prefix-icon="User" />
        </el-form-item>
        <el-form-item label="密码" prop="password" required>
          <ArInput v-model:value="form.password" type="password" prefix-icon="Lock" />
        </el-form-item>
        <el-button type="primary" class="login-btn" @click="onSubmit">登录</el-button>
      </el-form>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.login {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: #ccc;

  &-inner{
    margin-top: 20%;
    width: 300px;
    padding: 32px;
    background-color: #fff;
    border-radius: 4px;
  }

  &-btn {
    width: 100%;
  }
}
</style>

最后出来的页面效果即:

在这里插入图片描述

环境变量

在完成页面提交动作之前,先解决环境变量的问题,在测服、预发布或者生产中我们总有些变量是不一样的,所以需要做环境区分

新建env文件夹,内部新增三个四个文件:

.env                # 所有情况下都会加载
.env.development     # 开发环境
.env.release         # 预发布环境
.env.production      # 正服环境

举个例子:

# .env.development 
VITE_ENV = devalopment
# 请求接口
VITE_API_URL = https://api.vvhan.com/testapi/saorao

这样就可以在不同的环境中设置变量,然后我们修改一下脚本命令,区分一下环境:

"scripts": {
    "watch": "vite",
    "watch:release": "vite --mode release",
    "watch:production": "vite --mode production",
    "build:development": "vue-tsc && vite build --mode development",
    "build:release": "vue-tsc && vite build --mode release",
    "build:production": "vue-tsc && vite build --mode production",
    // ...
  },

在根目录新建build/utils.ts文件:

// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable) {
  const result: any = {}

  for (const envName of Object.keys(envConf)) {
    let realName = envConf[envName].replace(/\\n/g, '\n')
    realName =
      realName === 'true' ? true : realName === 'false' ? false : realName

    result[envName] = realName
    if (typeof realName === 'string') {
      process.env[envName] = realName
    } else if (typeof realName === 'object') {
      process.env[envName] = JSON.stringify(realName)
    }
  }
  return result
}

然后修改vite.config.js,将我们定义的变量注入进去:

import { defineConfig, loadEnv, UserConfig, ConfigEnv  } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { wrapperEnv } from './build/utils'

const resolve = (dist) => path.resolve(__dirname, dist)

// https://vitejs.dev/config/
export default ({ command, mode }: ConfigEnv): UserConfig => {
  const env = loadEnv(mode, './env')
  wrapperEnv(env)

  return {
    plugins: [
      vue(),
      AutoImport({
        imports: [
          'vue',
          'vue-router',
          'pinia'
        ],
        eslintrc: {
          enabled: false,
          filepath: './.eslintrc-auto-import.json',
          globalsPropValue: true
        },
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ],
    resolve: {
      alias: {
        '@': resolve('./src')
      },
      extensions: [".js", ".ts", ".tsx", ".jsx"]
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
        }
      }
    }
  }
}

这样我们就可以在下面的axios封装中使用了。哦对可能会有点ts的报错,修改tsconfig.node.json

"include": ["vite.config.ts", "build**/*.ts"]

axios封装

新建src/utils/request.ts

import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'

const request: AxiosInstance | any = axios.create({
  timeout: 100000,
  headers: {
    post: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  },
  withCredentials: true
});

request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  let token = localStorage.getItem("token");
  if (token && token !== '') {
    config.headers['Authorization'] = token;
  }

  // 获取环境变量!!!
  const projectUrlPrefix = import.meta.env.VITE_API_URL;
  // 这样更支持多域名接口的情况
  if (config && config.url && !/^(http(|s):\/\/)|^\/\//.test(config.url)) {
    config.url = projectUrlPrefix + config.url;
  }
  return config;
});

request.interceptors.response.use((res: any) => {
  if (res.data.status && res.data.status !== 200) {
    ElMessage.error(res.data.msg || '请求失败,请稍后重试')
    return Promise.reject(res.data)
  }
  // 如果这里是登录信息过期,那么应该给个弹窗提示什么的,最后都应该重定向到登录页面
  return res.data
}, (error: any) => {
  console.log(`%c 接口异常 `, 'background-color:orange;color: #FFF;border-radius: 4px;', error);
})

export default request;
export const $get = (url: string, params = {}) => {
  return request.get(url, {
    params
  })
}
export const $post = (url: string, params = {}) => {
  return request.post(url, params)
}

去登录页面做一下测试:

<script lang="ts" setup>
const $post = inject('$post') as any
// ...

const onSubmit = async () => {
  // ...
  const res = await $post('/login', form)
  // ...
}
</script >

测试不同环境下的域名前缀,如果不一样,则说明配置成功了~

https://api.vvhan.com/testapi/saorao/login
https://api.vvhan.com/releaseapi/saorao/login
https://api.vvhan.com/api/saorao/login

整体布局

在这里插入图片描述

  • 将路由模块中的路径转为菜单显示在左边;
  • 上面部分是用户信息,以及可以退出;
  • 切换路由,会出现一个类似浏览器的tab,可以点击tab切换,也可以关闭当前页面;
  • 最后是页面的主体内容显示。

先看看菜单组件:

<script lang='ts' setup>
import { ref, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import routes from '@/router/index';
import { IRouterItem } from '@/types/menu'
import { useTagViewsStore } from '@/store/tagViews';

const route = useRoute();
const router = useRouter();
const isCollapse = ref(false);
const defaultActive = ref('0')
const tagViewsStore = useTagViewsStore();
const { visitedViews } = storeToRefs(tagViewsStore);

// 1. 从声明的路由中获取当前显示的菜单栏(还可以过滤一些不是菜单栏的页面)
const currentMenu = computed(() => {
  if(routes && routes.length) {
    const routesArr: any = routes.filter((route) => route.name === 'Layout');
    if(routesArr && routesArr[0] && routesArr[0].children){
      const res = routesArr[0].children as IRouterItem[];
      return res;
    }else {
      return [];
    }
  }else {
    return []
  }
});

// 获取路由对应的菜单下标(如果一打开是客户列表页,则高亮客户列表)
const currentMenuToObj = computed(() => {
  const routes = currentMenu.value;
  if(routes && routes.length) {
    let obj: {[key: string]: any} = {};
    for(let i = 0; i < routes.length; i++) {
      const item = routes[i];
      if(item.children) {
        for(let j = 0; j< item.children.length; j++) {
          const subItem = item.children[j];
          obj[subItem.path] = {
            index: `${i}-${j}`,
            item: subItem
          };
        }
      }else {
        obj[item.path] = {
          index: '' + i,
          item
        }; 
      }
    }

    return obj;
  }else {
    return {};
  }
})

// 监听路由获取当前高亮的值,store的使用在后面详细说一下
watch(
  () => route.path,
  (val: string) => {
    if(!visitedViews.value.length) {
      const item = {
        path: '/',
        name: 'Index',
        meta: { title: '主页' },
      }
      tagViewsStore.addVisitedViews(item)
      tagViewsStore.setActivitedView(item)
    }
    if(val) {
      const obj = currentMenuToObj.value[val];
      defaultActive.value = obj.index;
      tagViewsStore.addVisitedViews(obj.item)
      tagViewsStore.setActivitedView(obj.item)
    }
  }, {
    immediate: true
  }
)

// 点击菜单栏进行跳转
const onToPage = (item: IRouterItem) => {
  if(route.path === item.path) return;
  tagViewsStore.addVisitedViews(item)
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <div class="menu">
    <div class="menu-logo">Logo</div>
    <div class="menu-main">
      <el-menu
        :default-active="defaultActive"
        :collapse="isCollapse"
        background-color="#191919"
        text-color="#7e7e7e"
        active-text-color="#ffffff"
      >
        <template v-for="(item, index) in currentMenu" :key="index">
          <el-menu-item :index="'' + index" v-if="!item.children || !item.children.length" @click="onToPage(item)">
            <template #title>{{ item.meta.title  }}</template>
          </el-menu-item>
          <el-sub-menu :index="'' + index" v-else>
            <template #title>
              <span>{{ item.meta.title  }}</span>
            </template>
            <el-menu-item v-for="(subItem, subIndex) in item.children" :key="`${index}-${subIndex}`" :index="`${index}-${subIndex}`" @click="onToPage(subItem)">{{ subItem.meta.title  }}</el-menu-item>
          </el-sub-menu>
        </template>
      </el-menu>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.menu {
  height: 100vh;
  max-width: 280px;
  background-color: #191919;
  
  &-logo {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
  }

  &-main {
    height: calc(100vh - 100px);
  }
}
</style>

<style lang="scss">
.menu .el-menu {
  border: none !important;
}
</style>

接下来是顶部栏信息:

<script lang='ts' setup>

</script>

<template>
  <div class="nav">
    <div class="nav-left"></div>
    <div class="nav-right">
      <div class="nav-right-item">
        <el-dropdown>
          <span class="el-dropdown-link">
            您好,XXX
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  height: 60px;
  border-bottom: solid 1px var(--el-menu-border-color);
  background-color: #fff;

  &-right{
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: flex-end;

    &-item {
      cursor: pointer;
    }
  }
}
</style>

接下来是关于导航栏tab的开发了,这里我们运用到了pinia来做状态管理,安装之后我们先在plugins/index.ts中注册:

import { createPinia } from 'pinia';

// ...
// 注册store (建议这个放在所有注册的首位,方便其他插件可能会用到它)
app.use(createPinia());

声明关于导航啦tabstore,在src/store/tagViews.ts中:

import { defineStore } from 'pinia';
import { IRouterItem } from '@/types/menu'

export const useTagViewsStore = defineStore('tagViews', {
  state: () => {
    return {
      // 访问过的页面
      visitedViews: [] as IRouterItem[],
      // 当前访问的页面
      activitedView: {} as IRouterItem
    }
  },

  actions: {
    // 新增页面
    addVisitedViews(view: IRouterItem){
      const item = this.visitedViews.find((item) => item.path === view.path)
      if(item) return;
      this.visitedViews.push(view);
    },
    
    // 删除页面
    deleteVisitedViews(index: number) {
      this.visitedViews.splice(index, 1);
    },
    
    // 高亮某个页面
    setActivitedView(view: IRouterItem) {
      this.activitedView = view
    }
  }
})

tagViews组件的源码如下:

<script lang='ts' setup>
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router'
import { useTagViewsStore } from '@/store/tagViews';
import { IRouterItem } from '@/types/menu'

const router = useRouter();
const tagViewsStore = useTagViewsStore();
const { visitedViews, activitedView } = storeToRefs(tagViewsStore);

// 关闭页面
const onDel = (item: IRouterItem) => {
  const index = visitedViews.value.findIndex((view) => view.path === item.path);
  if(index === -1) return;

  tagViewsStore.deleteVisitedViews(index);
  if(item.path === activitedView.value.path) {
    const obj = visitedViews.value[index - 1]
    tagViewsStore.setActivitedView(obj);
    router.push(obj.path)
  }
}

// 切换页面
const onChange = (item: IRouterItem) => {
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <el-scrollbar class="tags-scrollbar">
    <div class="tags">
      <div v-for="item in visitedViews" :key="item.path" :class="['tags-item', { active: activitedView.path === item.path }]" @click="onChange(item)" >
        <span class="tags-item-title">{{ item.meta ? item.meta.title : '' }}</span>
        <el-icon v-if="item.path !== '/'" @click.stop="onDel(item)"><Close /></el-icon>
      </div>
    </div>
  </el-scrollbar>
</template>

<style scoped lang='scss'>
.tags-scrollbar {
  height: 30px;
  overflow: hidden;
}
.tags {
  display: flex;
  background: #f3f3f3;
  border: 1px solid #f2f2f2;
  border-right: none;
  margin: -1px 0 0 -1px;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  &-item {
    display: flex;
    align-items: center;
    position: relative;
    cursor: pointer;
    height: 26px;
    line-height: 26px;
    border: 1px solid #d8dce5;
    color: #495060;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: 5px;
    margin-top: 4px;

    &-title {
      margin-right: 4px;
    }

    &:first-of-type {
      margin-left: 5px;
    }
    &:last-of-type {
      margin-right: 5px;
    }
    &.active {
      background-color: #e25050;
      color: #fff;
      border-color: #e25050;
      &::before {
        content: '';
        background: #fff;
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        position: relative;
        margin-right: 2px;
      }
    }
  }
}
</style>

pinia的使用比vuex简单了很多,最大的区别就是*mutations*不再存在了。

路由拦截

一般我们登录之后会将pinia中关于用户的信息进行更新,还会将一些信息进行加密之后存放在localstorage中。未登录的用户我们得拦截他们进入系统,并重定向到登录页面。(实际项目中还得考虑页面权限的拦截问题)

// router/hooks/index.ts
import type { Router } from 'vue-router'
import { USERINFO } from '@/constants/localstorage'

const routerHook = (router: Router) => {
  router.beforeEach(to => {
    if(to.path === '/login') {
      // 可以做一些清空登录信息的操作, 比如跟pinia相关的等操作
      localStorage.removeItem(USERINFO);
      return true;
    }else{
      // 在这里可以判断用户是否登录,跳转的某个页面是否有权限,这里只是粗略写一下
      const info = localStorage.getItem(USERINFO);
      if(!info) {
        return { name: 'Login' }
      }
    } 
  })
}

export default routerHook;


import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import routerHook from './hooks/index'

// 注册路由
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

routerHook(router)

export default router

修改一下登录页面,模拟一下:

const onSubmit = async () => {
  await formRef.value.validate(async (valid: boolean, fields: {[key: string]: any}) => {
    if (valid) {
      // 一般这种情况下,localStorage中存储的信息不能太重要,且需要加密,还应该更新pinia中的用户信息
      const info = {
        name: 'Armouy'
      }
      localStorage.setItem(USERINFO, JSON.stringify(info))
      router.push('/')
    } else {
      console.log('error submit!', fields)
    }
  })
}

未登录情况下都会重定向到登录页面。

打包

npm run build:production
npm run preview

参考链接

  • Eslint:vue3项目添加eslint(standard规则)
  • vite官网

如有错误,欢迎指出,感谢阅读~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/685217.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL Server数据库 -- 表的基础查询

文章目录 一、单表查询基本结构二、单表查询结构语法 select聚合函数where模糊查询order bygroup byhaving三、多表查询基本结构四、多表查询结构语法 内连接自连接外连接五、总结 前言 学习了数据库&#xff0c;在以后公司等地方&#xff0c;你可能不会用到创建数据库或者表格…

LeetCode 双周赛 107(2023/06/24)滑动窗口与离散化

本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。 往期回顾&#xff1a;LeetCode 单周赛第 348 场 数位 DP 模版学会了吗&#xff1f; T1. 最大字符串配对数目&#xff08;Easy&#xff09; 标签&…

FlutterUnit 已上架 iOS,暗色模式全面支持

theme: cyanosis 一、FlutterUnit 的全平台支持 FlutterUnit 是我的一个开源项目&#xff0c;基于 Flutter 构建的一个 全平台 应用程序。现在很荣幸地宣布: FlutterUnit 已经上架 iOS 的 App Store &#xff0c;自此主流的几大平台均已提供体验。 项目地址: https://github.co…

Chrome Edge Firefox Safari 如何清除 DNS 缓存

Chrome Edge Firefox Safari 如何清除 DNS 缓存 如何清除浏览器的 DNS 缓存 (Chrome, Firefox, Safari) Chrome Chromium Edge Firefox Safari clear DNS Cache, flush DNS cache 请访问原文链接&#xff1a;https://sysin.org/blog/clear-browser-dns-cache/&#xff0c;查…

前端Vue仿京东天猫商品属性选择器自定义单选按钮

前端Vue仿京东天猫商品属性选择器自定义单选按钮&#xff0c; 下载完整代码请访问uni-app插件市场地址&#xff1a;https://ext.dcloud.net.cn/plugin?id13176 效果图如下&#xff1a; # cc-radioBtnBox #### 使用方法 使用方法 <!-- attrArr&#xff1a;属性数据 clic…

改进YOLOv5/YOLOv8:复现结合即插即用 | 高效多尺度注意力(EMA),模块成为YOLOv5改进的小帮手

高效多尺度注意力(EMA) 论文介绍简介EMA模块图像分类实验目标检测实验yolov5加入方法yolo注册yaml文件3563.pdf](https://arxiv.org/ftp/arxiv/papers/2305/2305.13563.pdf) 论文介绍 通道或空间的显著有效性 注意机制对产生更多可辨识的 特征表示的显著效果,在各种计算机视…

深度学习-数据增强与扩充

数据增强可以说是数据驱动下的深度学习必经之路,掌握数据,相当于掌握当下主流方向的自动驾驶的命脉,是人工智能不可或缺的资源。本文将介绍最新的利用大模型扩充数据的方式! 先看下变色效果: 左褐色背景图 为原图,右侧为处理后的图! ​ AI day也在自动标注数据方面,着重…

软件测试技术-期末理论知识复习速成

第一章 软件测试概述 什么是软件缺陷&#xff0c;发生软件缺陷的原因是什么&#xff1f; 答&#xff1a;软件缺陷指的是在软件开发过程中出现的错误、缺陷或故障&#xff0c;导致软件无法按照预期的功能运行或者产生错误的结果。 导致软件缺陷的原因可能有&#xff1a;&#…

AI提高软件外包开发效率

最近几年AI技术取得了很大的进步&#xff0c;在一些领域甚至有突破性的进展&#xff0c;虽然无法预测未来AI会如何影响到人们的生活&#xff0c;但可以确定的是AI会在方方面面影响到大家的生活方式&#xff0c;也许未来五年内就会有一个明显的变化。今天和大家分享AI如何提高软…

IPv6地址配置与验证实验

IPv6地址配置与验证实验 【实验目的】 启动IPv6功能。配置IPv6地址。验证配置。 【实验拓扑】 实验拓扑如下图所示。 实验拓扑 设备参数如表所示。 设备参数表 设备 接口 IPv6地址 子网掩码位数 默认网关 R1 S0/1/0 2000:f106:f208:12::1 64 N/A R2 S0/1/0 2…

【软考网络管理员】2023年软考网管初级常见知识考点(9)- 接入网技术

涉及知识点 接入网相关技术&#xff0c;XDSL 接入&#xff0c;HFC 接入&#xff0c; PON 接入&#xff0c;软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多考点…

Spring源码:调度框架EnableSchedulingScheduled源码解析

目录 1.开启调度框架 2.ScheduledAnnotationBeanPostProcessor Bean后处理器分析 2.1 调度框架支持的Task类型 2.2 对Task进行调度执行 3.任务调度器 3.1 任务调度器获取 3.2 框架内提供的任务调度器 3.3 任务调度器执行逻辑 在实际项目开发中&#xff0c;有时会遇到定…

1、动手学深度学习——线性神经网络:线性回归的实现(从零实现+内置函数实现)

1、线性回归基本概念 回归&#xff08;regression&#xff09;是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域&#xff0c;回归经常用来表示输入和输出之间的关系。 给定一个数据集&#xff0c;我们的目标是寻找模型的权重和偏置&#xf…

selenium测试框架快速搭建(UI自动化测试)

一、介绍 selenium目前主流的web自动化测试框架&#xff1b;支持多种编程语言Java、pythan、go、js等&#xff1b;selenium 提供一系列的api 供我们使用&#xff0c;因此在web测试时我们要点页面中的某一个按钮&#xff0c;那么我们只需要获取页面&#xff0c;然后根据…

【力扣刷题 | 第十五天】

目录 前言&#xff1a; ​​​​​​​63. 不同路径 II - 力扣&#xff08;LeetCode&#xff09; 343. 整数拆分 - 力扣&#xff08;LeetCode&#xff09; 总结&#xff1a; 前言&#xff1a; 本篇我们主要刷动态规划的题&#xff0c;解题还是严格按照我们在【夜深人静写算法…

Linux查看文件大小

1、Linux下查看文件和文件夹大小 当磁盘大小超过标准时会有报警提示&#xff0c;这时如果掌握df和du命令是非常明智的选择。 df可以查看一级文件夹大小、使用比例、档案系统及其挂入点&#xff0c;但对文件却无能为力。 du可以查看文件及文件夹大小。 两者配合使用&#xf…

UE中创建可脚本化编辑器工具(Scriptable Tools)

UE5.2中提供了可脚本化工具编辑模式&#xff0c;该模式下用户可以编写蓝图节点自定义界面操作模式下的逻辑&#xff0c;例如重写鼠标点击事件&#xff0c;制作自定义画刷等。 如果你不太了解UE编辑器工具&#xff0c;可以参考这篇文章&#xff1a; https://blog.csdn.net/gray…

chatgpt赋能python:Python编程计算一元二次方程——最简单的方法实现

Python编程计算一元二次方程——最简单的方法实现 前言 Python编程语言是一种优秀的开源编程语言&#xff0c;具有易于学习、代码简洁明了、易于维护等优点&#xff0c;因此在近年来得到了广泛的应用。 本文将介绍如何使用Python编写一个简单而又实用的计算一元二次方程的程…

华为认证哪个方向考的人多?考试费用是多少?

从学校毕业后&#xff0c;就要走上社会了&#xff0c;很多人就要面临就业困难&#xff0c;对于信息通信行业的人来说&#xff0c;考一份技术证书&#xff0c;是可以帮助自己提高职业竞争力、获得好工作的方法。华为云认证是华为云旗下的认证&#xff0c;其设立时间长&#xff0…

多线程爬虫实战-思路

前言 最近有很多小伙伴找到我&#xff0c;说想要王者荣耀所有英雄皮肤的照片&#xff0c;但是又不想自己去下载&#xff0c;正好借这个机会给大家讲解一下多线程的爬虫实战 由于线程爬虫会对任何服务器都有一定的影响&#xff0c;本文仅供学习交流使用&#xff0c;切勿拿去做什…