首发于 Vue
vite --template vue & typescript 搭建项目 TSX

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 导入导出

  1. export default function fun(){ ... }
  2. export function fun(){ ... }
  3. export type { ... } from '...'
  4. export { ... } from '...'
  5. export { default as ..., { ... } } from '...'
  6. import ... from '...'
  7. 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 为什么是一个函数

vue 组件中的 data 数据都应该是相互隔离,互不影响的,组件每复用一次,data 数据就应该被复制一次,之后,当某一处复用的地方组件内 data 数据被改变时,其他复用地方组件的 data 数据不受影响,就需要通过 data 函数返回一个对象作为组件的状态。




35. vue 的 $nextTick 有什么用?

vue 中 this.$nextTick() 的实现和用法

在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的DOM。



37. npm 版本过低报错:npm ERR! code ERESOLVE

在命令后面加上:

--legacy-peer-deps



38. vue3.x 定义一个拖拽指令

自定义指令 | Vue.js

Vue3 自定义指令 | 菜鸟教程

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')
Vue3.x 定义一个拖拽指令 https://www.zhihu.com/video/1508515791644979200

面向 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 可进行展示状态切换。

vue2.x 写一个可拖拽浮窗 https://www.zhihu.com/video/1508821275502243840

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

渲染函数 & JSX — Vue.js

GitHub - vuejs / jsx-vue2

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

自定义事件 — Vue.js

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进程时的命令行参数:

  1. 数组的第一个元素 process.argv[0] ——返回启动Node.js进程的可执行文件所在的绝对路径
  2. 第二个元素 process.argv[1] ——为当前执行的JavaScript文件路径
  3. 剩余的元素为其他命令行参数



53 大数据表格 vxe-table



54. Alibaba Formily 阿里巴巴统一前端表单解决方案


55. Pinia

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: {