vite --template vue & typescript 搭建项目 TSX
~ Vite 官方中文文档
~ 本文配置仓库: 带只拖鞋去流浪/vite-vue-ts
/*
Option API
beforeCreate:数据还没有挂载呢,只是一个空壳
created:这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数
beforeMount:虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated
mounted:此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了
beforeUpdate:重新渲染之前触发,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染
updated:数据已经更改完成,dom也重新render完成
beforeDestory:销毁前执行($destroy方法被调用的时候就会执行),一般在这里善后:清除计时器、清除非指令绑定的事件等等...
destroyed:组件的数据绑定、监听...都去掉了,只剩下dom空壳,这里也可以善后
1. 创建项目

yarn create vite vite-vue3-ts --template vue

或者你可以直接
--template vue-ts
,就可以跳过第二步了(比较建议这样做)
yarn create vite vite-vue3-ts --template vue-ts
2. 安装配置 TypeScript
2.1 安装
yarn add typescript
2.2 生成 tsconfig.json
tsc --init # 在项目根目录下生成 tsconfig.json

2.3 关于 vite 的配置
{
"compilerOptions": {
"isolatedModules": true
}

2.4 一些关于 tsconfig.json 配置项
该配置参考
ant-design-vue
配置
{
"compilerOptions": {
"baseUrl": "./", /* 解析非相对模块的基地址,默认是当前目录 */
"isolatedModules": true, /* vite 编译器选项 */
"module": "esnext",
"strict": true,
"esModuleInterop": true, /* 允许 `export =` 导出,由 `import from` 导入 */
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": false, /* 不允许把 null、undefined 赋值给其他类型的变量 */
"experimentalDecorators": true,
"moduleResolution": "node", /* 模块解析策略,ts默认用node的解析策略,即相对的方式导入 */
"jsx": "preserve",
"noUnusedParameters": true, /* 检查未使用的函数参数(只提示、不报错) */
"noUnusedLocals": true, /* 检查只声明、未使用的局部变量(只提示、不报错) */
"noImplicitAny": false, /* 不允许隐式 Any 类型 */
"allowJs": true, /* 允许编译器编译 JS、JSX 文件 */
"target": "es6", /* 目标语言的版本 */
"lib": ["dom", "es2017"], /* TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array" */
"exclude": ["node_modules", "dist"] /* 指定编译器需要排除的文件或文件夹 */
}
2.5 为什么生成的 tsconfig.json 报错
files 属性作用是 指定需要编译的单个文件列表 。
但是默认情况下,需要编译项目下所有的 typescript 文件,并不需要指定 files。

3. 改造 App.vue 为 App.tsx
注意同时要修改 main.js 中的对于 App 的引入
import { defineComponent } from "@vue/runtime-core";
export default defineComponent({
name: 'App',
render(){
return (<div class="app">
<router-view></router-view> {/* 虽然暂时还没装 vue-router */}
</div>)
本文后面的组件都使用 TSX 来去写,都是用 defineComponent 去定义,不使用 *.vue 形式的单文件组件。
4. React is not defined?

4.1 安装
yarn add @vitejs/plugin-vue-jsx -D
4.2 配置 vite.config.js
+ import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [
vue(),
+ vueJsx()
5. vite 配置 resolve.alias

+ import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
然而这里用到了 TypeScript,直接上面那样配是会报错的,还需要在 tsconfig.json 中配置
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
}
6. 导入静态资源报错
续五

/* tsconfig.json */
"include": ["./src/declare/*"]
}
/* src/declare/image.d.ts */
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
7. 注册全局组件
我们直接利用 ESM 来导入导出 ,当然你也可以用 vite 的 Glob 导入

7.1 改造 HelloWorld

7.2 创建 components.ts
export { default as HelloWorld } from './HelloWorld'
7.3 创建 index.ts
import * as components from './components';
import type { App } from 'vue';
export const install = function (app: App) {
Object.keys(components).forEach((key:string) => {
const component = components[key];
app.component(key,component);
7.4 修改 main.js
+ import { install } from './components'
const app = createApp(App);
+ install(app);
app.mount('#app')
8. 安装配置 vuex
8.1 安装
yarn add vuex@next

8.2 store/index.ts
import { createStore } from 'vuex';
import * as modules from './modules';
export default createStore({
modules
})
8.3 store/modules/index.ts
export {default as app} from './app';
8.4 测试文件 store/modules/app.ts
import { SET_LOAD_NUM } from '../types'
const app = {
namespaced: true, // 命名空间
state: {
loadNum: 0
mutations: {
[SET_LOAD_NUM](state, data) {
state.loadNum = data;
actions: {
setLoadNum: ({ commit }, data) => commit(SET_LOAD_NUM, data)
getters: {
getLoadNum: state => state.loadNum
export default app;
8.5 类型文件(写一些 mutations 名称)store/types.ts
export const SET_LOAD_NUM = "SET_LOAD_NUM";
9. 安装配置 Less
yarn add less
10. 安装配置 tailwindcss
10.1 安装
yarn add tailwindcss
yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
10.3 生成配置文件 tailwind.config.js
npx tailwindcss init --full
10.3 导入
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

10.4 配置 postcss.config.js
module.exports = {
plugins: [
require("tailwindcss"),
require("autoprefixer"),
10.5 tailwind 类名提示

11. 安装配置 ant design vue
11.1 安装
yarn add ant-design-vue@next
11.2 配置
// main.js
+ import Antd from 'ant-design-vue';
+ import 'ant-design-vue/dist/antd.css';
+ app.config.productionTip = false;
+ app.use(Antd);
12. 安装配置 element-plus
如果不想使用 ant design vue,也可以使用 element-plus
12.1 安装
yarn add element-plus
12.2 全局引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
12.3 按需引入
需要另外安装插件
yarn add unplugin-vue-components
配置 vite.config.js
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
// ...
Components({
resolvers: [ElementPlusResolver()],
13. 安装配置 axios
13.1 安装
yarn add axios@next
13.2 配置
+ import axios from 'axios';
+ app.config.globalProperties.$http = axios;
13.3 api/index.ts
export function GET(app,url,params){
return app.$http.get(url,{ params });
export function POST(app,url,data){
return app.$http.post(url, data);
14. vite 配置代理 server.proxy

打个比方你可以这样配置 proxy

// vite.config.js
import appConfig from './public/static/app-config.json'
const { BASE_API: { host, port } } = appConfig;
export default defineConfig({
server: {
proxy: {
'/api': {
target: `${host}:${port}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
15. vite Glob 导入
我们在前面已经有提及。
关于
import.meta.glob
是懒加载的,而
import.meta.globEager
是直接引入所有模块。
以前面说到的注册全局组件为例:
// vite 的方法导入组件
const modules = import.meta.globEager('./*.tsx');
export default install = function(app) {
for (const path in modules) {
const mod = modules[path];
const compName = mod.default.name || path.replace(/^\.\/(.*)\.tsx$/, "$1");
const comp = mod.default;
app.component(compName, comp);

16. 指令要怎么使用
必须写齐全了,用单文件组件可以省略
v-model
为
:
,但在这里不可以
import { defineComponent } from "@vue/runtime-core";
import { computed, ref } from "vue";
export default defineComponent({
setup(){
const str = ref<String>('');
return { str }
render(){
return (
<input type="text" v-model:value={this.str} />
v-for
、
v-if
在这里已经不适用了,更多你可以查阅 JSX 语法。
v-for
可以用
arr.map(item=><>{item}</>)
替代,需要你返回 Array<Element>。
v-if
可以用三目运算符
bool ? <>1</> : <>2</>
替代。
v-slots
插槽可以看到
第十九(示例)
。
17. 安装配置 vue-router
17.1 安装
yarn add vue-router@next
17.2 router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routerHistory = createWebHistory();
const router = createRouter({
history: routerHistory,
routes: [{
path: '/',
redirect: '/home'
path: '/home',
component: () => import('@/views/Home')
path: '/example',
component: () => import('@/views/Example')
export default router
18. vuex 数据持久化
刷新前,保存到 sessionStorage,刷新后取出存到 vuex。(关于 cookie、localStorage、sessionStorage 区别请自行百度了解)
// App.tsx
setup(){
const store = useStore();
onMounted(()=>{
window.addEventListener('beforeunload',()=>{
sessionStorage.setItem("store", JSON.stringify(store.state));
if(sessionStorage.getItem('store')){
store.replaceState(Object.assign(store.state,JSON.parse(sessionStorage.getItem('store'))));
19. 使用 KeepAlive 缓存视图组件
如果你使用 *.jsx 开发,与原来 *.vue 模板引擎渲染的写法并不相同。
内置组件 | Vue.js :但我们这里需要引入

// App.tsx
import { defineComponent, KeepAlive } from "vue";
import { RouterView } from "vue-router";
export default defineComponent({
name: "App",
render() {
return (
<div class="App">
<RouterView>
({Component})=>{
return <KeepAlive include={['ServiceList']}>
<Component />
</KeepAlive>
</RouterView>
我们通过 KeepAlive 的 include 去指定我们需要缓存的页面,include 对应我们 createRouter 的 name:

20. KeepAlive 返回原页面回到原位
此处省略一大截,直接看到
beforeRouteLeave
和
activated
。思路:离开页面前保存 scrollTop 值,由于 keep-alive 缓存了整个组件,因此数据不会被刷新, 回到该页面后立即执行 activated,此时重新设置 scrollTop。
import { defineComponent, reactive } from "vue";
interface IState {
scroll?:number;
export default defineComponent({
name: "ServiceList",
setup() {
const state = reactive<IState>({
scroll: 0
return {
state
beforeRouteLeave(to,from,next){
this.state.scroll = (this.$refs.serviceList as Element).scrollTop;
next();
beforeRouteEnter(to, from, next) {
// 从其他地方过来的还是重新查询
next(vm=>{
if (!/^\/service\-detail\/[a-zA-Z0-9]+$/.test(from.path)) {
// @ts-ignore
vm.state.scroll = 0;
// @ts-ignore
vm.state.serviceDate = dayjs();
activated(){
// @ts-ignore
this.$refs.serviceList.scrollTop = this.state.scroll;
render() {
return (
<shell title="" v-slots={{
default: () => (
<div ref="serviceList" class="overflow-y-scroll h-full"></div>
</shell>
顺便把 JSX 使用插槽的展示在这里
// global component Shell
// Shell.tsx
import { defineComponent } from 'vue';
export default defineComponent({
name: "Shell",
setup( props, context ){
return {
props
render(){
return <>
<h1>{ props.title }</h1>
<div>{ $slots.default?.() }</div>
21. ESM 导入导出
-
export default function fun(){ ... }
-
export function fun(){ ... }
-
export type { ... } from '...'
-
export { ... } from '...'
-
export { default as ..., { ... } } from '...'
-
import ... from '...'
-
import { default as ..., {} } from '...'
大概就这些……
22. vue3.x composition api
在
setup
里面使用
22.1 响应式数据
-
ref
:单个数据 -
reactive
:对象
import
{ ref, reactive } from 'vue';
interface IState{
name: string;
age: number;
export default function App( props, context ){
let count = ref<number>(0);
let state = reactive<IState>({
name: '张三',
age: 22
return <></>;
22.2 router
-
useRouter
import { useRouter } from "vue-router";
export default function App( props, context ){
const router = useRouter();
return <></>;
22.3 store
-
useStore
import { useStore } from "vuex";
export default function App( props, context ){
const store = useStore();
return <></>;
22.4 计算属性
-
computed
import { ref, computed } from 'vue';
export default function App( props, context ){
let firstVal = ref<number>(1);
let secondVal = computed(()=> firstVal + 1);
return <>{ secondVal.value }</>;
22.5 监听
-
watch
-
watchEffect
-
watchSyncEffect
-
watchPostEffect
import { ref, watch, watchEffect } from 'vue';
export default function App( props, context ){
let val = ref<number>(1);
let valHistory = ref([]);
watch(()=>val, (val, preVal)=>{
valHistory.push(val);
return <>
<button onClick={ ()=>{ val++ } }>点我</botton>
valHistory.map(item => <div>{item}</div>)
22.6 生命周期

import { onMounted } from 'vue';
export default function App( props, context ){
onMounted(()=>{ });
return <></>;
其余不过多说明,详情可查阅 官方文档

23. Dayjs
这玩意是我看到 ant design 用了。删减版 Moment.js,体积要小很多,大部分常用 api 都有。

24. Moment.js

25. vscode:因为在此系统上禁止运行脚本
get-ExecutionPolicy
set-ExecutionPolicy RemoteSigned

26. 使用 font awesome 4
26.1 安装
yarn add font-awesome
26.2 引入
// main.js
import "font-awesome/css/font-awesome.min.css"
26.3 使用
<!-- 举例 fa-address-book -->
<i class="fa fa-address-book"></i>
27. lodash
Lodash 是一个著名的 javascript 原生库,不需要引入其他第三方依赖。是一个意在提高开发者效率,提高 javascript 原生方法性能的 javascript 库。简单的说就是,很多方法 lodash 已经帮你写好了,直接调用就行,不用自己费尽心思去写了,而且可以统一方法的一致性。Lodash使 用了一个简单的
_
符号,就像 jQuery 的
$
一样,十分简洁。
类似的还有 Underscore.js 和 Lazy.js。
28. i18n 国际化
Vue I18n:
如果你使用了 Ant Design of Vue(ConfigProvider):
29. video.js
Video.js 是一个为 HTML5 世界从头开始构建的网络视频播放器。它支持 HTML5 视频和现代流媒体格式,以及 YouTube、Vimeo 甚至 Flash(通过插件)。
30. swiper.js
Swiper常用于移动端网站的内容触摸滑动
Swiper是纯javascript打造的滑动特效插件,面向手机、平板电脑等移动终端。Swiper能实现触屏焦点图、触屏Tab切换、触屏轮播图切换等常用效果。Swiper开源、免费、稳定、使用简单、功能强大,是架构移动终端网站的重要选择!
31. strapi
偶然发现,了解中……
Strapi - Open source Node.js Headless CMS
32. vue-apollo:graphql
在项目中使用 graphql 接口时,可以使用 Vue Apollo
33. vue 响应式原理

34. 为什么 vue 的 data 是一个函数?
vue 组件中的 data 数据都应该是相互隔离,互不影响的,组件每复用一次,data 数据就应该被复制一次,之后,当某一处复用的地方组件内 data 数据被改变时,其他复用地方组件的 data 数据不受影响,就需要通过 data 函数返回一个对象作为组件的状态。
35. vue 的 $nextTick 有什么用?
在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的DOM。
37. npm 版本过低报错:npm ERR! code ERESOLVE
在命令后面加上:
--legacy-peer-deps

38. vue3.x 定义一个拖拽指令
import { createApp } from 'vue'
const app = createApp({})
// 注册
app.directive('my-directive', {
// 指令是具有一组生命周期的钩子:
// 在绑定元素的 attribute 或事件监听器被应用之前调用
created() {},
// 在绑定元素的父组件挂载之前调用
beforeMount() {},
// 绑定元素的父组件被挂载时调用
mounted() {},
// 在包含组件的 VNode 更新之前调用
beforeUpdate() {},
// 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
updated() {},
// 在绑定元素的父组件卸载之前调用
beforeUnmount() {},
// 卸载绑定元素的父组件时调用
unmounted() {}
// 注册 (功能指令)
app.directive('my-directive', () => {
// 这将被作为 `mounted` 和 `updated` 调用
// getter, 如果已注册,则返回指令定义
const myDirective = app.directive('my-directive')

面向 fixed 实现拖拽
import { createApp } from 'vue'
const app = Vue.createApp({})
app.directive('drag', {
mounted(el) {
el.onmousedown = function (e1) {
// mouse position in this
const mouseBoxX = e1.offsetX;
const mouseBoxY = e1.offsetY;
document.onmousemove = (e2) => {
const mouseBodyX2 = e2.clientX;
const mouseBodyY2 = e2.clientY;
var x = mouseBodyX2 - mouseBoxX;
var y = mouseBodyY2 - mouseBoxY;
const { width, height } = window.getComputedStyle(el);
const containerWidth = document.body.clientWidth;
const containerHeight = document.body.clientHeight;
console.log(width, height,containerWidth,
containerHeight);
const { x: left, y: top } = getPositionWithBoundary({
minX: 0,
minY: 0,
maxX: containerWidth,
maxY: containerHeight,
X: x,
Y: y,
width: parseInt(width),
height: parseInt(height),
el.style.left = left+'px';
el.style.top = top+'px';
document.onmouseup = function () {
this.onmousemove = this.onmouseup = null;
function getPositionWithBoundary(
{ minX, minY, maxX, maxY, X, Y, width, height } = {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
X: 0,
Y: 0,
width: 0,
height: 0,
if (X < minX) X = minX;
if (X + width > maxX) X = maxX - width;
if (Y < minY) Y = minY;
if (Y + height > maxY) Y = maxY - height;
return {
x: X,
y: Y,
39. vue2.x 写一个可拖拽浮窗
与38节一样,该拖拽浮窗面向 fixed 使用。这是继 React 之后写的( 带只拖鞋去流浪:WebGIS:OpenLayers 实现弹窗(React) )一样的设计思想。增加背景透明度调节、双击 header 可进行展示状态切换。

39.1 属性、插槽、事件说明
属性 | 类型 | 说明 |
---|---|---|
height | props/Number/500 | dragBox 高度 |
width | props/Number/800 | dragBox 宽度 |
className | props/String/"" | dragBox 类名 |
styles | props/Object/{} | dragBox 行内样式 |
headerClassName | props/String/"" | dragBox header类名 |
headerStyles | props/Object/{} | dragBox haeder行内样式 |
bodyClassName | props/String/"" | dragBox body类名 |
bodyStyles | props/Object/{} | dragBox body行内样式 |
title | props/String/"标题" | dragBox 标题 |
titleClassName | props/String/"" | dragBox title类名 |
titleStyles | props/Object/{} | dragBox title行内样式 |
backgroundOpacity | props/Number/0.8 | dragBox 背景透明度 |
actions | slot | dragBox header actions(右上角,除了三个默认外) |
body | slot | dragBox body |
close | event | dragBox 关闭 |
39.2 how to use
<!-- isShow state -->
<DragBox title="查询结果" className="search-result" :height="550" @close="isShow = false" v-show="isShow" >
<template v-slot:actions>
<!-- 你自定义的actions -->
</template>
<template class="content" @click.stop @mousedown.stop v-slot:body>
<!-- 你要展示在窗体中的内容 body -->
</template>
</DragBox>
39.3 源码
<template>
@author songgh @带只拖鞋去流浪
@description 拖拽
:class="[
'drag-box',
className,
'mini-drag-box': showType === 'mini',
'full-drag-box': showType === 'full',
ref="box"
:style="{
...styles,
top: py,
left: px,
height: `${height}px`,
width: `${width}px`,
<section
:class="['box-header', headerClassName]"
:style="{
'background-color': `rgba(64, 0, 255, ${backgroundOpacity})`,
...headerStyles,
ref="header"
@dblclick="
showType = ['full', 'mini'].includes(showType) ? 'normal' : 'full'
:class="['box-title', titleClassName]"
:style="{
...titleStyles,
{{ title }}
<section class="box-actions" @mousedown.stop>
<slot name="actions"></slot>
<div class="must-actions">
class="el-icon-minus action"
v-if="['normal', 'full'].includes(showType)"
@click.stop="showType = 'mini'"
class="el-icon-copy-document action"
v-if="['mini', 'full'].includes(showType)"
@click.stop="showType = 'normal'"
class="el-icon-full-screen action"
v-if="['mini', 'normal'].includes(showType)"
@click.stop="showType = 'full'"
<i class="el-icon-close action" @click="$emit('close')"></i>
</div>
</section>
</section>
<section
:class="['box-body', bodyClassName]"
:style="{
'background-color': `rgba(255, 255, 255, ${backgroundOpacity})`,
...bodyStyles,
@mousedown.stop
<slot name="body"></slot>
</section>
</div>
</template>
<script>
* @author songgh
* @description 计算边界
const getPositionWithBoundary = (
{ minX, minY, maxX, maxY, X, Y, width, height } = {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
X: 0,
Y: 0,
width: 0,
height: 0,
) => {
if (X < minX) X = minX;
if (X + width > maxX) X = maxX - width;
if (Y < minY) Y = minY;
if (Y + height > maxY) Y = maxY - height;
return {
x: X,
y: Y,
export default {
name: "WarningBox",
props: {
height: {
type: Number,
default: 500,
width: {
type: Number,
default: 800,
className: {
type: String,
default: "",
styles: {
type: Object,
default: () => ({}),
headerClassName: {
type: String,
default: "",
headerStyles: {
type: Object,
default: () => ({}),
bodyClassName: {
type: String,
default: "",
bodyStyles: {
type: Object,
default: () => ({}),
title: {
type: String,
default: "标题",
titleClassName: {
type: String,
default: "",
titleStyles: {
type: Object,
default: () => ({}),
// show: {
// type: Boolean,
// default: false,
// },
backgroundOpacity: {
type: Number,
default: 0.8,
data() {
return {
px: "calc(50% - 400px)",
py: "calc(50% - 250px)",
showType: "normal",
mounted() {
this.$refs.header.onmousedown = this.handleMouseDown;
methods: {
* @author songgh
* @description 移动弹窗
handleMouseDown(e1) {
// mouse position in this
const mouseBoxX = e1.offsetX;
const mouseBoxY = e1.offsetY;
document.onmousemove = (e2) => {
const mouseBodyX2 = e2.clientX;
const mouseBodyY2 = e2.clientY;
const x = mouseBodyX2 - mouseBoxX;
const y = mouseBodyY2 - mouseBoxY;
const { width, height } = window.getComputedStyle(this.$refs?.box);
const containerWidth = document.body.clientWidth;
const containerHeight = document.body.clientHeight;
const { x: left, y: top } = getPositionWithBoundary({
minX: 0,
minY: 0,
maxX: containerWidth,
maxY: containerHeight,
X: x,
Y: y,
width: parseInt(width),
height: parseInt(height),
this.$refs.box.style.left = left + "px";
this.$refs.box.style.top = top + "px";
document.onmouseup = function () {
this.onmousemove = this.onmouseup = null;
computed: {},
</script>
<style lang="scss" scoped>
.drag-box {
position: fixed;
border-radius: 10px;
z-index: 101;
user-select: none;
z-index: 9999;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.05);
.box-header {
height: 48px;
box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.05);
border-radius: 10px 10px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
cursor: move;
.box-title {
color: #fff;
line-height: 48px;
.box-actions {
display: flex;
justify-content: center
;
align-items: center;
.must-actions {
margin-left: 8px;
.action {
color: #000;
margin-left: 8px;
cursor: pointer;
height: 24px;
width: 24px;
line-height: 24px;
text-align: center;
border-radius: 100%;
&:nth-child(1) {
background-color: rgb(68, 255, 0);
&:nth-child(2) {
background-color: rgb(255, 234, 0);
&:nth-child(3) {
background-color: rgb(255, 64, 0);
.box-body {
padding: 16px;
border-top: none;
border-radius: 0 0 10px 10px;
height: calc(100% - 81px);
.mini-drag-box {
width: 320px !important;
height: auto !important;
.box-header {
border-radius: 10px;
.box-body {
display: none;
.full-drag-box {
width: 100vw !important;
height: 100vh !important;
top: 0 !important;
left: 0 !important;
background-color: #fff;
.box-header {
border-radius: 0;
.box-body {
border-radius: 0;
</style>
40. vscode 修改完文件后都会生成对应的 dist 或 xxx.dev.js 文件

41. vue2.x 使用 jsx
41.1 安装 babel 插件
cnpm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props --save-dev
41.2 配置 babel.config.js
module.exports = {
presets: [ '@vue/cli-plugin-babel/preset', ['@vue/babel-preset-jsx', { 'injectH': false }] ]
41.3 *.vue 文件写 jsx
<script>
// Test.vue
export default {
name: 'Test',
components: {},
directives: {},
filters: {},
// 数据
props: {},
inject: [],
provider: () => ({});
data: () => ({}),
computed: {},
watch: {},
// 生命周期
beforeCreate(){},
created(){},
beforeMount(){},
mounted(){},
beforeUpdate(){},
updated(){},
beforeDestory(){},
destory(){},
// keep-alive
activated(){},
deactivated(){},
// 渲染
render(){
const description = 'test component';
return <div class="test">{description}</div>;
</script>
<style lang="scss" scoped>
@import './test.scss';
// 建议分开文件写
</style>
// test.scss
.test{ }
41.4 vue-cli 支持 css-in-javascript

配置参考 | Vue CLI 、 CSS 相关 | Vue CLI
// test.module.scss
.test{
// 一些样式
.testLeft{}
.testRight{}
}
// Test.jsx
import styles from './test.module.scss'
export default {
name: 'Test',
data(){
return {};
render(){
return (<div class={styles.test}>
<section class={styles.testLeft}></section>
<section class={styles.testRight}></section>
</div>);
41.5 推荐用 *.jsx 文件写函数式组件
写单文件组件是不够灵活的,且不易维护的
41.6 使用 jsx
下面这篇文章够用了
// props 传一些传不了props。比如 element-ui form 的 :model 传不了,用这个可以传
// on 绑定一些绑定不上的事件。
<Test props={{}} on={{}} />
41.7 element-ui table slot-scope 在 jsx 中

API — Vue.js slot-scope 已废弃,推荐 v-slot

v-slot 在 jsx 中用 scopedSlots 替代
42. SectionForm 组件 (vue2.x、element-ui)
vue2 与 element-ui 配置。使用 jsx,堆屎山不好。
建议使用 model/emit/mixin 对以下代码进行改造【2023-03-29】。
42.1 index.jsx
import _ from "lodash";
import styles from './index.module.scss';
* @description 配置 Form(已支持 SFC / slot 配置)
* @author songgh
export default {
name: "section-form",
components: {
MaqueCascader,
UploadCommentPictureCard,
props: {
formOptions: {
type: Object,
default: () => ({
formData: {},
formPropList: [],
data() {
return {
userId: undefined,
formData: this.getFormData(),
mounted() {
this.userId = window.localStorage.getItem("userId");
methods: {
getFormData() {
let formData = {};
_.forEach(_.flatten(this.formOptions?.formPropList), item => {
if (!item?.name) return true;
formData[item.name] = this.formOptions?.formData[item.name];
if (item?.inputType === "number" && item?.precision) {
formData[item?.name] = (+formData[item?.name] || 0).toFixed(
item.precision
const { elAppend, elPrepend } = item;
if (elAppend && elAppend?.name) {
formData[elAppend.name] = this.formOptions?.formData[elAppend.name];
if (elPrepend && elPrepend?.name) {
formData[elPrepend.name] = this.formOptions?.formData[elPrepend.name];
return {
...formData
// 执行提交表单
submitForm() {
let flag = false;
this.$refs.form?.validate((valid) => {
if (valid) {
flag = true;
} else {
console.error("formData validate error!");
return false;
if (flag) {
return { ...this.formData }
} else {
throw new Error("必填缺失")
* @description 所有表单验证
$validate(){
let flag = false;
this.$refs.form?.validate((valid) => {
if (valid) {
flag = true;
} else {
console.error("formData validate error!");
return false;
if (flag) {
return { ...this.formData }
} else {
throw new Error("必填缺失")
* @description 对部分表单验证
$validateField(){
* @description 获取 formData
$formData() {
return { ...this.formData };
* @description 重置表单
$resetFields(){
this.$refs.form?.resetFields();
* @description 移除验证
$clearValidate(arr){
if(_.isArray(arr)){
this.$refs.form?.clearValidate(arr);
}else{
this.$refs.form?.clearValidate();
handleInputBlur(item) {
// 精度处理
if (item?.inputType === "number" && item?.precision) {
this.formData[item?.name] = Math.abs(this.formData[item?.name]).toFixed(
item.precision
item?.blur && item.blur(_.get(item, "value"));
renderInput(item) {
return (
<el-input
placeholder={_.get(item, "placeholder", "请输入")}
clearable={item?.clearable}
size={item?.size || "small"}
readonly={item?.readonly}
type={item?.inputType || "text"}
v-model={this
.formData[item?.name]}
disabled={item?.disabled}
style={{ ...item?.style }}
step={_.get(item, "step", 1)}
max={_.get(item, "max")}
min={_.get(item, "min")}
scopedSlots={{
suffix() {
return item?.suffix;
prefix() {
return item?.prefix;
on={{
blur: this.handleInputBlur.bind(this, { ...item })
{/* suffix */}
{(() => {
if (item?.suffix) {
return <div slot="suffix">{item.suffix}</div>;
} else {
try {
const element = this.$scopedSlots[item?.suffixSlotName]();
return <div slot="suffix">{element}</div>;
} catch (error) {
null;
})()}
{/* prefix */}
{(() => {
if (item?.prefix) {
return <div slot="prefix">{item.prefix}</div>;
} else {
try {
const element = this.$scopedSlots[item?.prefixSlotName]();
return <div slot="prefix">{element}</div>;
} catch (error) {
null;
})()}
{/* prepend */}
{(() => {
if (item?.elPrepend) {
return (
<div slot="prepend">
{item.elPrepend?.rerender
? this.renderFormItemContent(item.elPrepend)
: item.elPrepend}
} else {
try {
const element = this.$scopedSlots[item?.prependSlotName]();
return <div slot="prepend">{element}</div>;
} catch (error) {
null;
})()}
{/* append */}
{(() => {
if (item?.elAppend) {
return (
<div slot="append">
{item.elAppend?.rerender
? this.renderFormItemContent(item.elAppend)
: item.elAppend}
} else {
try {
const element = this.$scopedSlots[item?.appendSlotName]();
return <div slot="append">{element}</div>;
} catch (error) {
null;
})()}
</el-input>
renderInputNumber(item) {
return (
<el-input-number
controls={_.get(item, 'controls', false)}
v-model={this.formData[item?.name]}
disabled={item?.disabled}
precision={_.get(item, "precision", 6)}
step={_.get(item, "step", 1)}
min={_.get(item, "min", 0)}
placeholder={_.get(item, "placeholder", "请输入")}
style={{ ...item?.style }}
size={item?.size || "small"}
></el-input-number>
renderSelect(item) {
return (
<el-select
size={item.size || "small"}
placeholder={_.get(item, 'placeholder', "请选择")}
value-key={item?.valueKey}
clearable={item?.clearable}
multiple={item?.multiple}
collapse-tags={item?.collapseTags}
multiple-limit={item?.multipleLimit}
v-model={this.formData[item?.name]}
style={{ ...item?.style }}
on={{
change: (val) => {
item.change && item.change(val);
disabled={item?.disabled}
{_.map(item?.options, (option,
optionKey) => {
return (
<el-option
key={optionKey}
label={option?.label}
value={option?.value}
></el-option>
</el-select>
renderCascader(item) {
return (
<el-cascader
style={{ ...item?.style }}
v-model={this.formData[item?.name]}
size={item?.size || "small"}
options={item?.options || []}
placeholder={_.get(item, 'placeholder', "请选择")}
props={item?.props}
disabled={item?.disabled}
clearable={item?.clearable}
></el-cascader>
renderDatePicker(item) {
return (
<el-date-picker
style={{ ...item?.style }}
size={item?.size || "small"}
disabled={item?.disabled}
v-model={this.formData[item?.name]}
format={item?.format || "yyyy-MM-dd"}
value-format={item?.valueFormat || "yyyy-MM-dd"}
type={item?.pickerType || "date"}
placeholder={_.get(item, 'placeholder', "选择日期")}
></el-date-picker>
renderCheckboxGroup(item) {
return (
<el-checkbox-group
style={{ ...item?.style }}
v-model={this.formData[item?.name]}
{_.map(item?.options, (optionItem, key) => {
return (
<el-checkbox label={optionItem.label} key={key}>
{optionItem.label}
</el-checkbox>
</el-checkbox-group>
renderRadioGroup(item) {
return (
<el-radio-group
style={{ ...item?.style }}
v-model={this.formData[item?.name]}
on={{
change: (val) => {
item.change && item.change(val);
{_.map(item?.options, (optionItem, key) => {
return (
<el-radio label={optionItem.value} key={key}>
{optionItem.label}
</el-radio>
</el-radio-group>
renderSwitch(item) {
return (<el-switch
style={{ ...item?.style }}
v-model={this.formData[item?.name]}
active-color={item?.activeColor || "#13ce66"}
inactive-color={item?.inactiveColor || "#ff4949"}
inactive-value="0"
active-value="1"
on={{
change: (val) => {
item.change && item.change(val);
</el-switch>)
renderFormItemContent(item) {
return (
<div class={["form-item", item?.className]}>
{(() => {
if (item?.prefixHtml) {
return item.prefixHtml;
} else {
try {
const element = this.$scopedSlots[item?.prefixHtmlSlotName]();
return element;
} catch (error) {
null;
})()}
{item.type === "input" && this.renderInput(item)}
{item.type === "input-number" && this.renderInputNumber(item)}
{item.type === "select" && this.renderSelect(item)}
{item.type === "radio-group" && this.renderRadioGroup(item)}
{item.type === "checkbox-group" && this.renderCheckboxGroup(item)}
{item.type === "cascader" && this.renderCascader(item)}
{item.type === "date-picker" && this.renderDatePicker(item)}
{item.type
=== "maque-cascader" && this.renderMaqueCascader(item)}
{item.type === "maque-xzq" && this.renderMaqueXzq(item)}
{item.type === "upload" && this.renderUploadComment(item)}
{item.type === "upload-text" && this.renderUploadGroup(item)}
{item.type === "switch" && this.renderSwitch(item)}
{(() => {
if (item?.html) {
return item.html;
} else {
try {
const element = this.$scopedSlots[item?.htmlSlotName]();
return element;
} catch (error) {
null;
})()}
{(() => {
if (item?.suffixHtml) {
return item.suffixHtml;
} else {
try {
const element = this.$scopedSlots[item?.suffixHtmlSlotName]();
return element;
} catch (error) {
null;
})()}
render() {
const formProps = {};
if (this.formOptions?.disabled) formProps.disabled = this.formOptions.disabled;
return (
<div class={styles.sectionForm}>
<el-form
ref="form"
label-position={this.formOptions?.labelPosition || "right"}
inline={this.formOptions?.inline}
props={{
model: { ...this.formData },
...formProps
rules={this.formOptions?.rules || {}}
{_.map(this.formOptions?.formPropList, (rowData, key) => {
return (rowData &&
<el-row gutter={16} key={key}>
{_.map(rowData, (item, itemKey) => {
return (item &&
<el-col
key={itemKey}
span={item.span || 24 / rowData.length}
<el-form-item
label={item?.label}
prop={item?.name}
label-width={
item.labelWidth || this.formOptions?.labelWidth
{this.renderFormItemContent(item)}
</el-form-item>
</el-col>
</el-row>
</el-form>
<div>{this._formData}</div>
watch: {
"formOptions": {
handler() {
this.formData = this.getFormData();
deep: true,
immediate: true,
42.2 index.module.scss
.sectionForm {
:global {
.el-form {
padding: 0px 5px;
.el-input {
.el-select {
.el-input {
min-width: 130px;
.el-cascader {
.el-input {
min-width: 90px;
/* 谷歌 */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
/* 火狐 */
input {
-moz-appearance: textfield;
.el-input-number{
width: 100% !important;
.el-input__inner{
text-align: left !important;
}
42.3 使用

import SectionForm from "@/components/SectionForm/index.jsx"
import { mapState } from 'vuex';
import _ from 'lodash';
* @author songgh
export default {
name: 'ZdInfo',
components: {
SectionForm,
props: {
formData: {
type: Object,
required: true,
default: () => ({}),
methods: {
$submit() {
const formData = this.$refs.form?.submitForm();
if (formData?.xzqDm && (formData.xzqDm instanceof Array)) {
formData.xzqDm = _.last(formData.xzqDm);
return
formData;
render() {
return <SectionForm ref="form" formOptions={this.formOptions} />
computed: {
// disabled
...mapState({
disabled: state => state?.spaceUnit?.disabled
formOptions() {
return {
labelWidth: "98px",
labelPosition: "left",
disabled: this.disabled,
rules: {
xzqDm: [
{ required: true, message: "必填项", trigger: "blur" }
zdBh: [
{ required: true, message: "必填项", trigger: "blur" }
zdMj: [
{ required: true, message: "必填项", trigger: "blur" }
zdZl: [
{ required: true, message: "必填项", trigger: "blur" }
formData: { ...this.formData },
formPropList: [
type: "cascader",
label: "行政区",
name: "xzqDm",
options: JSON.parse(localStorage.getItem("sqXzq")),
props: { expandTrigger: "hover" },
type: "input",
label: "电子监管号",
name: "dzBaBh",
disabled: true,
type: "input",
label: "宗地编号",
name: "zdBh",
], [
type: "input",
inputType: "number",
label: "宗地面积",
name: "zdZmj",
suffix: '公顷',
span: 8,
type: "input",
label: "宗地坐落",
name: "zdZl",
placeholder: "请输入宗地的四至",
span: 8,
}
42.4 @form-create
发现已有开源可配置的表单,form-create 分别有基于 element-ui、iview、ant design vue
43. SectionTable 组件 (vue2.x、element-ui)
vue2 与 element-ui 配置。使用 jsx,堆屎山不好。
建议使用 mixin 对以下代码进行改造【2023-03-29】。
43.1 index.jsx
import table2xlsx from "@/utils/table2xlsx.js";
import _ from "lodash";
import styles from "./sectionTable.module.scss";
import uuid from "@/utils/uuid";
* @description 配置 Table(已支持 SFC / slot 配置)
* @author songgh
export default {
name: "SectionTable",
props: {
title: {
type: String,
default: "表格"
tableData: {
type: Array,
required: true,
default: () => []
tableColumns: {
type: Array,
required: true,
default: () => []
height: {
type: Number
pagination: {
type: Boolean,
default: false
pageSize: {
type: Number,
default: 5
total: {
type: Number,
default: 0
highlightCurrentRow: {
type: Boolean,
default: false
size: {
type: String,
default: "5"
border: {
type: Boolean
multiple: {
type: Boolean,
default: false
cellStyle: {
type: Object,
default: () => ({})
cellClassName: {
type: Function,
default: () => {}
headerCellStyle: {
type: Object,
default: () => ({})
treeProps: {
type: Object,
default: () => ({})
rowKey: {
type: String,
default: ""
showHeader: {
type: Boolean,
default: true
lazy: {
type: Boolean,
default: false
spanMethod: { type: Function, default: () => {} },
loads: { type: Function, default: () => {} },
expandChange: { type: Function, default: () => {} }
data() {
return {
currentPage: 1,
tableIndex: uuid()
watch: {
tableColumns: {
handler() {
this.tableIndex = uuid();
deep: true,
immediate: true
render() {
return (
<div class={styles.sectionTable}>
<el-table
ref="table"
key={this.tableIndex}
data={this.tableData}
height={this.height}
header-cell
-class-name={this.headerCellClassName}
highlight-current-row={this.highlightCurrentRow}
border={this.border}
size={this.size}
on={{
"current-change": this.handleCurrentChange,
"selection-change": this.handleSelectionChange,
"expand-change": this.expandChange
cell-style={this.cellStyle}
header-cell-style={this.headerCellStyle}
// 获取子表格数据
load={this.loads}
// 合并行或列
span-method={this.spanMethod}
tree-props={this.treeProps}
lazy={this.lazy}
row-key={this.rowKey}
show-header={this.showHeader}
cell-class-name={this.cellClassName}
{this.multiple && (
<el-table-column type="selection" width="55"></el-table-column>
{this.renderTableColumns(this.tableColumns)}
</el-table>
{this.pagination && this.renderPagination()}
methods: {
hanldeExport() {
table2xlsx({ filename: this.title, target: this.$refs.table });
handleCurrentChange(val) {
this.$emit("changeCurrent", val);
handleSelectionChange(val) {
this.$emit("changeSelection", val);
handlePageChange(val) {
this.currentPage = val;
this.$emit("pageChange", {
currentPage: val
renderTableColumns(tableColumns) {
return _.map(tableColumns, (item, key) => {
return (
<el-table-column
key={key}
type={item?.type}
prop={item?.name}
label={item?.label}
align={item?.align || "left"}
width={item?.width || "auto"}
scopedSlots={{
default: props => {
if (item?.itemRender) {
return item?.itemRender(props?.row, props?.$index);
} else {
try {
const element = this.$scopedSlots[item?.defaultSlotName]({
rowData: props?.row,
$index: props?.$index
return element;
} catch (error) {
return props?.row[item?.name];
header: props => {
if (item?.itemHeaderRender) {
return item?.itemHeaderRender(props?.row, props?.$index);
} else {
try {
const element = this.$scopedSlots[item?.headerSlotName]({
rowData: props?.row,
$index: props?.$index
return element;
} catch (error) {
return props?.column?.label;
{this.renderTableColumns(_.get(item, "children", []))}
</el-table-column>
renderPagination() {
return (
<el-pagination
background={true}
class={styles.pagination}
layout="prev, pager, next"
total={this.total}
page-size={this.pageSize}
current-page={this.currentPage}
on={{
"current-change": this.handlePageChange
></el-pagination>
43.2 index.module.scss
.sectionTable {
:global {
.header-cell {
background-color: rgba(156, 170, 211, 0.2);
color: #363839;
.current-row{
font-weight: bold;
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
43.3 table2xlsx.js
yarn add xlsx,我这里用这个库导出 table
/**
* @author songgh
* @description export table as excel
import { Message } from 'element-ui';
import * as XLSX from 'xlsx';
export default function table2xlsx({ filename, target } = { filename: "表格" }) {
if (!target) {
Message.error('没有指定 table');
return;
// clone 表格
const targetTable = target?.$el;
const cloneTable = targetTable.cloneNode();
cloneTable.innerHTML = `${targetTable.innerHTML}`;
// 移除 fix
const fix = cloneTable.querySelector(".el-table__fixed");
if(fix) cloneTable.removeChild(fix);
// 导出
var wb = XLSX.utils.table_to_book(cloneTable);
/* generate file and force a download*/
XLSX.writeFile(wb, `${filename}.xlsx`);
}
43.4 uuid.js
/**
* @author songgh
* @returns uuid
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
export default uuid;
43.5 使用

import SectionTable from "@/components/SectionTable/index.jsx";
* @author songgh
export default {
name: "SourceManagement",
data() {
return {
tableData:[],
tableColumns: [
{ label: "项目名称", name: "xmMc" },
{ label: "供地面积", name: "gyMj" },
label: "操作",
itemRender: (rowData, $index) => (
<el-button
type="danger"
size="mini"
onClick={this.handleDelete.bind(this, "tableData", $index)}
</el-button>
itemHeaderRender: () => (
<el-button
type="primary"
size="mini"
onClick={this.handleAdd.bind(this, "tableData")}
</el-button>
render(){
return <SectionTable
tableColumns={this.tableColumns}
tableData={this.tableData}
size="mini"
border={true}
hedaerCellStyle={{}}
methods: {
* @description 点击新增按钮
* @param {*} refName
handleAdd(refName) {
this.$refs[refName]?.setVisible(true);
* @description 点击删除按钮
* @param {*} listName
* @param {*} $index
handleDelete(listName, $index) {
this[listName]?.splice($index, 1);
44. vue2.x 状态管理(vuex 版本3)
44.1 开启严格模式
严格模式 | Vuex vue2.x 中使用的是 vuex3.x
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
getters: {},
modules: {},
// 非生产模式下开启严格模式
// 会检测 *.vue 里面
strict: process.env.NODE_ENV !== 'production',
44.2 命名空间管理

从里往外看
store/modules/business-handling/landSupplyScheme.js:一个 module
/* store/modules/business-handling/landSupplyScheme.js */
import {
SET_DISABLED
} from './types.js';
export default {
namespaced: true,
state: {
disabled: true, // disabled
mutations:{
// setDisabled
[SET_DISABLED](state, { disabled }){
state.disabled = disabled;
actions: {
setDisabled: ({commit}, {payload}) => {
commit(SET_DISABLED, payload)
getters: {
getDisabled: state => state?.disabled,
store/modules/business-handling/types.js:mutations 名称
/* store/modules/business-handling/types.js */
export const SET_DISABLED = "SET_DISABLED";
store/modules/index.js:导出所有 modules
/* store/modules/index.js */
export {
default as landSupplyScheme } from './business-handling/landSupplyScheme.js';
store/index.js:注册 store
/* store/index.js */
import Vue from 'vue'
import Vuex from 'vuex'
import * as modules from './modules/index.js';
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
...modules,
strict: true,
基于上面的命名空间管理,下面来介绍使用
44.3 dispatch:调用 actions
this.$store.dispatch({
type: "landSupplyScheme/setDisabled",
payload: {
disabled: true,
44.4 commit:调用 mutations
但是我不用,如上述所示,我只需要调用 actions -> mutations
this.$store.commit("landSupplyScheme/SET_DISABLED", { disabled: true } );
44.5 getters
const disabled = this.$store.getters["landSupplyScheme/getDisabled"];
44.6 mapState:组件计算属性中获取 state

// component.jsx
import { mapState } from 'vuex';
export default {
computed:{
// disabled
...mapState({
disabled: state => state?.landSupplyScheme?.disabled
render(){
return <Button type="primary" disabled={this.disabled}>按钮</Button>
45. vue2.x 封装一个基础 echarts 组件
自动 resize,自动更新;只需要传入 options 即可。
props | 说明 |
---|---|
options | echarts 的配置 |
import * as echarts from "echarts";
import styles from './chart.module.scss';
* @description 基础 echart
* @author 知乎 @带只拖鞋去流浪
export default {
name: "BaseChart",
props: {
options: {
type: Object,
required: true,
default: () => ({}),
data() {
return {
chart: null,
methods: {
resize() {
setTimeout(() => {
this.chart?.resize();
}, 0);
initEchart() {
const target = this.$refs.chart;
if (!target) return;
if (this.chart) {
window.removeEventListener("resize", this.resize);
this.chart?.dispose();
this.chart = echarts?.init(target);
this.chart?.setOption(this.options);
window.addEventListener("resize", this.resize);
mounted() {
this.initEchart();
beforeDestroy() {
window.removeEventListener("resize", this.resize);
render() {
return <div class={styles.chart} ref="chart" />;
watch:{
options: {
handler(){
this.$nextTick(() => {
this.initEchart();
deep: true,
immediate: true,
.chart{
width: 100%;
height: 100%;
min-width: 300px;
min-height: 300px;
}
46. vue2.x 自定义组件的 v-model

46.1 el-input-plus
由于 el-input 没有对数值进行精度处理,现在对 el-input 进行二次封装
// el-input-plus.js
Vue.component('el-input-plus', {
model: {
prop: 'value',
event: 'blur'
props: {
value: {
type: String | Number,
default: ''
placeholder: {
default: "请输入",
type: String,
size: {
default: "default",
type: String,
type: {
default: "input",
type: String,
max: {
type: String
| Number,
min: {
type: String | Number,
step: {
type: String | Number,
default: 1
disabled: { type: Boolean, default: false, },
precision: { type: String | Number },
data() {
return {
newValue: ''
watch:{
value: {
handler(value){
this.newValue = value;
this.handleBlur();
immediate: true,
methods: {
handleBlur() {
if (this.type === 'number') {
if (this.min > this.newValue) {
this.newValue = this.min;
if (this.max < this.newValue) {
this.newValue = this.max;
if (this.precision) {
this.newValue = (Number(this.newValue) || 0).toFixed(
this.precision
console.log(this.precision, this.newValue);
this.$emit("blur", this.newValue);
this.$emit('change', this.newValue)
this.$emit('input', this.newValue)
template: `
<div class="el-input-plus">
<el-input
v-model="newValue"
:placeholder="placeholder"
:size="size"
:type="type"
:max="max"
:min="min"
:step="step"
:disabled="disabled"
@blur="() => {
handleBlur();
@focus="$emit('focus')"
<template slot="prefix">
<slot name="prefix"></slot>
</template>
<template slot="suffix">
<slot name="suffix"></slot>
</template>
<template slot="prepend">
<slot name="prepend"></slot>
</template>
<template slot="append">
<slot name="append"></slot>
</template>
</el-input>
/* el-input-plus.css */
.el-input-plus{
display: inline-block;
/* 谷歌 */
.el-input-plus input::-webkit-outer-spin-button,
.el-input-plus input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
/* 火狐 */
.el-input-plus input {
-moz-appearance: textfield;
47. vue2.x 使用 typescript
配置参考 | Vue CLI 、 @vue/cli-plugin-typescript
下面这篇文章够用了:

48. vue2.x 关于 render 渲染函数
48.1 vue-loader
Vue Loader 是一个 webpack 的 loader,它允许你以一种名为 单文件组件 (SFCs) 的格式撰写 Vue 组件。
48.2 vue-template-compiler
此包可用于将 Vue 2.0 模板预编译为渲染函数,以避免运行时编译开销和 CSP 限制。在大多数情况下,您应该将它与 一起使用
vue-loader
,如果您正在编写具有非常特定需求的构建工具,则仅需要单独使用它。
48.3 template 编译 virtual dom 过程
template -> AST(抽象语法树) -> render(h) -> virtual dom -> diff -> dom
48.4 render 编译 virtual dom 过程
render(h) -> virtual dom -> diff -> dom
使用 babel-plugin-transform-vue-jsx 进行转化
48.5 AST 是什么东西?
AST 即 抽象语法树,本质上是一个JS对象
<!DOCTYPE html>
<html lang="en">
<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>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<div id="root" class="App">
<p>你叫什么名字?</p>
<p
>{{msg}}</p>
</div>
<script>
var app = new Vue({
el: '#root',
data: {
msg: "我叫张三"
console.log(app)
</script>
</body>
</html>
49. elemefe

50. 对 axios 进行封装
51. ProTable 组件 (vue2.x、element-ui)
结合 SectionForm 和 SectionTable 写一个弹窗搜索组件
51.1. index.jsx
/**
* @description 弹窗搜索列表(已支持 SFC / slot)
* @author songgh
import _ from "lodash";
import SectionTable from "@/components/SectionTable";
import SectionForm from "@/components/SectionForm";
export default {
name: "ModalForm",
components: {
SectionTable,
SectionForm
props: {
tableColumns: {
type: Array,
default: () => []
pageSize: {
type: String,
default: "5"
size: {
type: String,
default: "small"
getData: {
type: Function,
default: async () => {},
required: true
formOptions: {
type: Object,
default: () => ({}),
required: true
multiple: {
type: Boolean,
default: false
spanMethod: {
type: Function,
default: () => {}
highlightCurrentRow: {
type: Boolean,
default: true
hedaerCellStyle: {
type: Object,
default: () => ({
"background-color": "rgba(156, 170, 211, 0.2)",
color: "#363839"
cellClassName: {
type: Function,
default: () => {}
data() {
return {
tableData: [],
total: 0,
currentPage: 1,
selectList: [],
formData: {}
methods: {
handleSelect(val) {
this.selectList = _.isArray(val) ? val : [val];
handleSearch() {
this.currentPage = 1;
this.handlePageChange({ currentPage: 1 });
async handlePageChange({ currentPage }) {
this.currentPage = currentPage;
const { total, list } = await this.getData({
currentPage: currentPage,
pageSize: this.pageSize,
...this.$refs.form?.$formData()
this.total = total;
this.tableData = list;
render() {
return (
<SectionForm
ref="form"
formOptions={this.formOptions}
scopedSlots={{
...this.$scopedSlots
<SectionTable
tableData={this.tableData}
tableColumns={this.tableColumns}
on={{
pageChange: this.handlePageChange,
changeCurrent: this.handleSelect,
changeSelection: this.handleSelect
pagination={true}
total={this.total}
size={this.size}
page-size={Number(this.pageSize)}
highlightCurrentRow={this.highlightCurrentRow}
cell-class-name={this.cellClassName}
multiple={this.multiple}
span-method={this.spanMethod}
hedaerCellStyle={this.hedaerCellStyle}
scopedSlots={{
...this.$scopedSlots
52.2. 使用

<script>
* @description 选择储备出库单编号
* @author songgh
import ProTable from "@/components/ProTable";
import { mapState } from "vuex";
import * as service from "@/service/business-handling/space-unit.js";
import _ from "lodash";
export default {
components: {
ProTable
data() {
return {
tableColumns: [
{ label: "行政区", name: "xzqMc" },
{ label: "宗地编号", name: "zdBh" },
{ label: "出库面积", name: "ckMj" }
selectData: []
methods: {
async getData(params) {
try {
var { data } = await service.getCkdList({
pageSize: params.pageSize,
pageNum: params.currentPage,
instance: {
...params,
xzqDm: _.isArray(params.xzqDm) ? _.last(params.xzqDm) : params.xzqDm,
ckdBh: _.trim(params.ckdBh),
zdBh: _.trim(params.zdBh)
} catch (error) {
console.error(error);
return {
total: _.get(data, "total", 0),
list: _.get(data, "list", [])
setVisible(flag) {
this.$refs.proTable?.setVisible(true);
render() {
return (
<ProTable
ref="proTable"
on={{
getModelVal: val => {
this.selectData = val;
const [target] = val;
this.$emit("change", target);
title="选择储备出库单编号"
tableColumns={this.tableColumns}
getData={this.getData}
formOptions={this.formOptions}
multiple={false}
computed: {
// disabled
...mapState({
disabled: state => state?.spaceUnit?.disabled
formOptions() {
return {
labelWidth: "98px",
labelPosition: "left",
inline: true,
disabled: this.disabled,
rules: {},
formData: {
xzqDm: _.padEnd(window.localStorage.getItem("userxzq"), 6, '0'),
formPropList: [
type: "cascader",
label: "行政区",
name: "xzqDm",
options: JSON.parse(localStorage.getItem("sqXzq")),
props: { expandTrigger: "hover" }
type: "input",
label: "出库单编号",
name: "ckdBh"
type: "input",
labelWidth: "110px",
label: "宗地编号",
name: "zdBh"
html: (
<el-button
size="small"
type="primary"
onClick={() => this.$refs.proTable?.handleSearch()}
</el-button>
</script>
52. node 自定义启动参数
我们希望启动项目和打包项目进入不同的入口。因此在命令行输入一个变量。

52.1 process.argv
process 对象是一个全局变量,它提供当前 Node.js 进程的有关信息,以及控制当前 Node.js 进程。
process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数:
- 数组的第一个元素 process.argv[0] ——返回启动Node.js进程的可执行文件所在的绝对路径
- 第二个元素 process.argv[1] ——为当前执行的JavaScript文件路径
- 剩余的元素为其他命令行参数
53 大数据表格 vxe-table
54. Alibaba Formily 阿里巴巴统一前端表单解决方案
55. Pinia
作为新的状态管理工具。Pinia 抛弃了 mutation,actions 既可以是同步也可以是异步;且无 namespaced / modules 概念,采用扁平化管理状态,更好的维护性。
import { defineStore } from 'pinia';
* import { useStore } from '@/store'
* const store = useStore()
* 重置store:store.$reset()
* sotre.$patch({}) / store.$patch((state) => {})
export const useStore = defineStore('main', {
state: () => ({}),
// getters 可以用来进行一些计算的操作
getters: {
// 没有 mutation
// 没有 modules
// actions 可以是同步也可以是异步
// actions 异步调用接口,处理响应结果
actions: {