# vue3+ts项目

# EditorConfig编辑配置

让不同电脑系统不同编译器保证代码风格一致。 官网 (opens new window)

配置.editorconfig文件,编辑器会自动读取,不过vscode需要安装插件:EditorConfig for VS Code

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

# 使用prettier工具

vscode 安装插件 Prettier - Code formatter

Prettier 是一款强大的代码格式化工具,支持 JavaScript、CSS、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定。

  1. 安装prettier
npm install prettier -D
  1. 配置.prettierrc文件:
  • useTabs:使用tab缩进还是空格缩进,选择false;
  • tabWidth:tab是空格的情况下,是几个空格,选择2个;
  • printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
  • singleQuote:使用单引号还是双引号,选择true,使用单引号;
  • trailingComma:在多行输入的尾逗号是否添加,设置为 none
  • semi:语句末尾是否要加分号,默认值true,选择false表示不加;
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "none",
  "semi": false
}
  1. 创建.prettierignore忽略文件
/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*
  1. 测试prettier是否生效
  • 测试一:在代码中保存代码;
  • 测试二:配置一次性修改的命令;

在package.json中配置一个scripts:

    "prettier": "prettier --write ."

# 使用ESLint检测

  1. VSCode需要安装ESLint插件:
  2. 解决eslint和prettier冲突的问题:

安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)

npm i eslint-plugin-prettier eslint-config-prettier -D

添加prettier插件:

  extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint",
    'plugin:prettier/recommended'
  ],

# git Husky和eslint

虽然已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:

  • 也就是我们希望保证代码仓库中的代码都是符合eslint规范的;

  • 那么我们需要在组员执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;

那么如何做到这一点呢?可以通过Husky工具:

  • husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push

如何使用husky呢?

这里可以使用自动配置命令:

npx husky-init
npm install

这里会做三件事:

接下来,需要去完成一个操作:在进行commit时,执行lint脚本:

  "scripts": {
    "lint": "vue-cli-service lint --fix",
    "prepare": "husky install"
  },

.husky>pre-commit

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

# git commit规范 Commitizen

  • Commitizen
cnpm install commitizen -D
  • 安装cz-conventional-changelog,并且初始化cz-conventional-changelog:
npx commitizen init cz-conventional-changelog --save-dev --save-exact

这个命令会帮助安装cz-conventional-changelog:

并且在package.json中进行配置:

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint --fix",
    "prettier": "prettier --write . ",
    "prepare": "husky install"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "commitizen": "^4.2.5",
    "cz-conventional-changelog": "^3.3.0",
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
}

这个时候提交代码需要使用 npx cz:

Type 作用
feat 新增特性 (feature)
fix 修复 Bug(bug fix)
docs 修改文档 (documentation)
style 代码格式修改(white-space, formatting, missing semi colons, etc)
refactor 代码重构(refactor)
perf 改善性能(A code change that improves performance)
test 测试(when adding missing tests)
build 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)
ci 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
chore 变更构建流程或辅助工具(比如更改测试环境)
revert 代码回退

# 代码提交验证

如果按照cz来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?

可以通过commitlint来限制提交;

  1. 安装 @commitlint/config-conventional 和 @commitlint/cli
cnpm i @commitlint/config-conventional @commitlint/cli -D
  1. 在根目录创建commitlint.config.js文件,配置commitlint
module.exports = {
  extends: ['@commitlint/config-conventional']
}
  1. 使用husky生成commit-msg文件,验证提交信息:
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

可能的问题 (opens new window)

因为这条语句里包含着$1,它是在shell命令中代表的是参数。而Windows系统的cmd是没有$1这种操作符。

首先要搞明白这条命令做了什么事情:

  • 添加一个文件commit-msg
  • 然后再commit-msg中填充该npx指令

解决:直接把步骤分开执行就可以解决

  • 添加一个文件commit-msg在.husky文件夹下, 执行下面命令
npx husky add .husky/commit-msg
  • 在创建后的文件夹commit-msg文件里直接填充你的指令即可:npx --no-install commitlint

# 项目中eslint.js其他配置

module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/typescript/recommended',
    '@vue/prettier',
    '@vue/prettier/@typescript-eslint',
    'plugin:prettier/recommended'
  ],
  parserOptions: {
    ecmaVersion: 2020
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    '@typescript-eslint/no-var-requires': 'off',//项目中有使用require,eslint想要统一成import,会报红色警告,可以配置关闭
    '@typescript-eslint/no-explicit-any': 'off'
  }
}

# ts中创建路由

import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'// 这是vue-router内部定义好的 
// 加上type表示这是类型而不是函数等,type可以省略

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    component: () => import('@/views/login/login.vue')
  },
  {
    path: '/main',
    component: () => import('@/views/main/main.vue')
  }
]

const router = createRouter({
  routes,
  history: createWebHashHistory()
})

export default router

# shims-vue.d.ts

配置了某些插件,在template中直接使用$store会报红,需要在这里声明配置一下

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

declare let $store: any

# ts封装axios

实现目标:

  1. 返回值约束,比如res.date时报错,避免代码写错
  2. 传入约束
// 请求出口
import HYRequest from './request'
// 基本配置根据不同的环境返回不同的url timeout
import { BASE_URL, TIME_OUT } from './request/config'

const hyRequest = new HYRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  interceptors: {
    requestInterceptor: (config) => {
      // 携带token的拦截
      const token = ''
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }

      console.log('请求成功的拦截')
      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('请求失败的拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('响应成功的拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('响应失败的拦截')
      return err
    }
  }
})

export default hyRequest

  • request/index.ts
import axios from 'axios'
import type { AxiosInstance } from 'axios'
//type
import type { HYRequestInterceptors, HYRequestConfig } from './type'

import { ElLoading } from 'element-plus'
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'

const DEAFULT_LOADING = true

class HYRequest {
  instance: AxiosInstance
  interceptors?: HYRequestInterceptors
  showLoading: boolean
  loading?: ILoadingInstance

  constructor(config: HYRequestConfig) {
    // 创建axios实例
    this.instance = axios.create(config)

    // 保存基本信息
    this.showLoading = config.showLoading ?? DEAFULT_LOADING
    this.interceptors = config.interceptors

    // 使用拦截器
    // 1.从config中取出的拦截器是对应的实例的拦截器
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )

    // 2.添加所有的实例都有的拦截器
    this.instance.interceptors.request.use(
      (config) => {
        console.log('所有的实例都有的拦截器: 请求成功拦截')

        if (this.showLoading) {
          this.loading = ElLoading.service({
            lock: true,
            text: '正在请求数据....',
            background: 'rgba(0, 0, 0, 0.5)'
          })
        }
        return config
      },
      (err) => {
        console.log('所有的实例都有的拦截器: 请求失败拦截')
        return err
      }
    )

    this.instance.interceptors.response.use(
      (res) => {
        console.log('所有的实例都有的拦截器: 响应成功拦截')

        // 将loading移除
        this.loading?.close()

        const data = res.data
        if (data.returnCode === '-1001') {
          console.log('请求失败~, 错误信息')
        } else {
          return data
        }
      },
      (err) => {
        console.log('所有的实例都有的拦截器: 响应失败拦截')
        // 将loading移除
        this.loading?.close()

        // 例子: 判断不同的HttpErrorCode显示不同的错误信息
        if (err.response.status === 404) {
          console.log('404的错误~')
        }
        return err
      }
    )
  }

  request<T>(config: HYRequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 1.单个请求对请求config的处理
      if (config.interceptors?.requestInterceptor) {
        config = config.interceptors.requestInterceptor(config)
      }

      // 2.判断是否需要显示loading
      if (config.showLoading === false) {
        this.showLoading = config.showLoading
      }

      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 1.单个请求对数据的处理
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }
          // 2.将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEAFULT_LOADING

          // 3.将结果resolve返回出去
          resolve(res)
        })
        .catch((err) => {
          // 将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEAFULT_LOADING
          reject(err)
          return err
        })
    })
  }

  get<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  delete<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  patch<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}

export default HYRequest

  • type.ts
import type { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface HYRequestInterceptors<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: HYRequestInterceptors<T>
  showLoading?: boolean
}

# tsconfig.json

{
  "compilerOptions": {
    // 目标代码(ts -> js(es5/6/7))
    "target": "esnext",
    // 目标代码需要使用的模块化方案(commonjs require/module.exports/es module import/export)
    "module": "esnext",
    // 严格一些严格的检查(any)
    "strict": true,
    // 对jsx进行怎么样的处理
    "jsx": "preserve",
    // 辅助的导入功能
    "importHelpers": true,
    // 按照node的方式去解析模块 import "/index.node"
    "moduleResolution": "node",
    // 跳过一些库的类型检测 (axios -> 类型/ lodash -> @types/lodash / 其他的第三方)
    // import { Person } from 'axios'
    "skipLibCheck": true,
    // export default/module.exports = {}
    // es module 和 commonjs
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    // 要不要生成映射文件(ts -> js)
    "sourceMap": true,
    // 文件路径在解析时, 基本url
    "baseUrl": ".",
    // 指定具体要解析使用的类型
    "types": ["webpack-env"],
    // 路径解析(类似于webpack alias)
    "paths": {
      "@/*": ["src/*"],
      "components/*": ["src/components/*"]
    },
    // 可以指定在项目中可以使用哪里库的类型(Proxy/Window/Document)
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": ["node_modules"]
}

# ts+vue3 获取子组件的实例

  • 读取子组件的属性内容:ref<InstanceType<typeof LoginAccount>>(),因为是没赋值,所以调用子组件的方法时accountRef.value?.loginAction()加问号表示不存在时短路
<template>
  <div class="login-panel">
      <template >
        <span>账号登录</span>
        <login-account ref="accountRef" />
      </template>
    <el-button  @click="handleLoginClick"
      >立即登录</el-button
    >
  </div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import LoginAccount from './login-account.vue'
export default defineComponent({
  components: {
    LoginAccount
  },
  setup() {
    //InstanceType<typeof LoginAccount> === 获取了LoginAccount的实例
    const accountRef = ref<InstanceType<typeof LoginAccount>>()
    const handleLoginClick = () => {
      accountRef.value?.loginAction()
    }
    return {
      handleLoginClick,
      accountRef
    }
  }
})
</script>

当然也可以应用在elementplus的组件上

const formRef = ref<InstanceType<typeof ElForm>>()

    const loginAction = () => {
      formRef.value?.validate((valid) => {
        if (valid) {
          console.log('真正执行登录逻辑')
        }
      })
    }

# ts+vue3使用vuex

  • IRootState接口
export interface IRootState {
  name: string
  age: number,
}
  • Vuex
import { createStore } from 'vuex'

import login from './login/login'

import { IRootState } from './types'
// createStore可以传入泛型约束state,那么就更好的控制
const store = createStore<IRootState>({
  state() {
    return {
      name: 'coderwhy',
      age: 18,
      love:'ll'//这边不会报错
    }
  },
  mutations: {
    af(state){
      // console.log(state.love)
      //类型“IRootState”上不存在属性“love”。
    }
  },
  getters: {},
  actions: {},
  modules: {
    login
  }
})

export function setupStore() {
  store.dispatch('login/loadLocalLogin')
}

export default store
  • Login.ts
// 引入Module,ts对vuex的支持还是需要更多的人为操作
import { Module } from 'vuex'

import {
  accountLoginRequest,
  requestUserInfoById,
  requestUserMenusByRoleId
} from '@/service/login/login'
import localCache from '@/utils/cache'
import router from '@/router'

import { IAccount } from '@/service/login/type'
import { ILoginState } from './types'
import { IRootState } from '../types'
// 子模块需要支持自己的state和根模块的state
const loginModule: Module<ILoginState, IRootState> = {
  namespaced: true,
  state() {
    return {
      token: '',
      userInfo: {},
      userMenus: []
    }
  },
  getters: {},
  mutations: {
    changeToken(state, token: string) {
      state.token = token
    },
    changeUserInfo(state, userInfo: any) {
      state.userInfo = userInfo
    },
    changeUserMenus(state, userMenus: any) {
      state.userMenus = userMenus
    }
  },
  actions: {
    async accountLoginAction({ commit }, payload: IAccount) {
      // 1.实现登录逻辑
      const loginResult = await accountLoginRequest(payload)
      const { id, token } = loginResult.data
      commit('changeToken', token)
      localCache.setCache('token', token)

      // 2.请求用户信息
      const userInfoResult = await requestUserInfoById(id)
      const userInfo = userInfoResult.data
      commit('changeUserInfo', userInfo)
      localCache.setCache('userInfo', userInfo)

      // 3.请求用户菜单
      const userMenusResult = await requestUserMenusByRoleId(userInfo.role.id)
      const userMenus = userMenusResult.data
      commit('changeUserMenus', userMenus)
      localCache.setCache('userMenus', userMenus)

      // 4.跳到首页
      router.push('/main')
    },
    loadLocalLogin({ commit }) {
      const token = localCache.getCache('token')
      if (token) {
        commit('changeToken', token)
      }
      const userInfo = localCache.getCache('userInfo')
      if (userInfo) {
        commit('changeUserInfo', userInfo)
      }
      const userMenus = localCache.getCache('userMenus')
      if (userMenus) {
        commit('changeUserMenus', userMenus)
      }
    }
  }
}

export default loginModule

# vuex使用useStore的类型检测

  • /login/types
export interface ILoginState {
  token: string
  userInfo: any
  userMenus: any
}
  • 所有的接口类型交叉之后导出给useStore方法使用
import { ILoginState } from './login/types'

export interface IRootState {
  name: string
  age: number
}

export interface IRootWithModule {
  // 这里可以继续增加其他模块所需要的属性
  login: ILoginState
}
//交叉类型进行处理
export type IStoreType = IRootState & IRootWithModule
import { createStore, Store, useStore as useVuexStore } from 'vuex'

import login from './login/login'

import { IRootState, IStoreType } from './types'

const store = createStore<IRootState>({
  state() {
    return {
      name: 'coderwhy',
      age: 18
    }
  },
  modules: {
    login
  }
})

export function setupStore() {
  store.dispatch('login/loadLocalLogin')
}
//使用自己的useStore
export function useStore(): Store<IStoreType> {
  return useVuexStore()
}
export default store
import { defineComponent, computed } from 'vue'
// 使用自己已经处理过的useStore那样就会增加类型检测了
import { useStore } from '@/store'
 setup() {
    const store = useStore()
    const userMenus = computed(() => store.state.login.userMenus)
    return {
      userMenus
    }
  }

# ts+vue3请求接口的封装

export interface IAccount {
  name: string
  password: string
}

export interface ILoginResult {
  id: number
  name: string
  token: string
}

export interface IDataType<T = any> {
  code: number
  data: T
}

import hyRequest from '../index'

import { IAccount, IDataType, ILoginResult } from './type'

enum LoginAPI {
  AccountLogin = '/login',
  LoginUserInfo = '/users/', // 用法: /users/1
}

export function accountLoginRequest(account: IAccount) {
  //泛型嵌套
  return hyRequest.post<IDataType<ILoginResult>>({
    url: LoginAPI.AccountLogin,
    data: account
  })
}

export function requestUserInfoById(id: number) {
  return hyRequest.get<IDataType>({
    url: LoginAPI.LoginUserInfo + id,
    showLoading: false
  })
}
  //post<IDataType<ILoginResult>>这里的T就是上面get/post后跟的T,就是为了约束返回值data的类型
  post<T>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

# ts+vue3 PropType: props增加具体对象数组限制

利用propType,它可以通过泛型进行对父组件传过来的格式进行约束。

import { defineComponent, PropType } from 'vue'
type IFormType = 'input' | 'password' | 'select' | 'datepicker'

export interface IFormItem {
  type: IFormType
  label: string
  rules?: any[]
  placeholder?: any
  // 针对select
  options?: any[]
  // 针对特殊的属性
  otherOptions?: any
}

export default defineComponent({
  props: {
    formItems: {
      type: Array as PropType<IFormItem[]>,
      default: () => []
    }
  },
  setup() {
    return {}
  }
})

# ts+vue3 table封装

export const contentTableConfig = {
  title: '用户列表',
  propList: [
    { prop: 'name', label: '用户名', minWidth: '100' },
    { prop: 'realname', label: '真实姓名', minWidth: '100' },
    { prop: 'cellphone', label: '手机号码', minWidth: '100' },
    { prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' },
    {
      prop: 'createAt',
      label: '创建时间',
      minWidth: '250',
      slotName: 'createAt'
    },
    {
      prop: 'updateAt',
      label: '更新时间',
      minWidth: '250',
      slotName: 'updateAt'
    },
    { label: '操作', minWidth: '120', slotName: 'handler' }
  ],
  showIndexColumn: true,
  showSelectColumn: true
}
<template>
  <div class="hy-table">
   
    <el-table
      :data="listData"
      border
      style="width: 100%"
      @selection-change="handleSelectionChange"
    >
      <el-table-column
        v-if="showSelectColumn"
        type="selection"
        align="center"
        width="60"
      ></el-table-column>
      <el-table-column
        v-if="showIndexColumn"
        type="index"
        label="序号"
        align="center"
        width="80"
      ></el-table-column>
      <template v-for="propItem in propList" :key="propItem.prop">
        <el-table-column v-bind="propItem" align="center">
          <template #default="scope">
            <slot :name="propItem.slotName" :row="scope.row">
              {{ scope.row[propItem.prop] }}
            </slot>
          </template>
        </el-table-column>
      </template>
    </el-table>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    title: {
      type: String,
      default: ''
    },
    listData: {
      type: Array,
      required: true
    },
    propList: {
      type: Array,
      required: true
    },
    showIndexColumn: {
      type: Boolean,
      default: false
    },
    showSelectColumn: {
      type: Boolean,
      default: false
    }
  },
  emits: ['selectionChange'],
  setup(props, { emit }) {
    const handleSelectionChange = (value: any) => {
      emit('selectionChange', value)
    }

    return {
      handleSelectionChange
    }
  }
})
</script>
 <hy-table :listData="dataList" v-bind="contentTableConfig">
      <!--列中的插槽 -->
      <template #status="scope">
        <el-button
          plain
          size="mini"
          :type="scope.row.enable ? 'success' : 'danger'"
        >
          {{ scope.row.enable ? '启用' : '禁用' }}
        </el-button>
      </template>
      <template #createAt="scope">
        <span>{{ $filters.formatTime(scope.row.createAt) }}</span>
      </template>
      <template #updateAt="scope">
        <span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
      </template>
      <template #handler>
        <div class="handle-btns">
          <el-button icon="el-icon-edit" size="mini" type="text"
            >编辑</el-button
          >
          <el-button icon="el-icon-delete" size="mini" type="text"
            >删除</el-button
          >
        </div>
      </template>
    </hy-table>
	
props: {
	    contentTableConfig: {
	      type: Object,
	      require: true
	    }
},

# ts+vue3 对象表单的双向绑定处理

# 子组件v-model变化通知父组件更新

+ 子组件
<template>
    <el-form :label-width="labelWidth">
			<el-input
			  :placeholder="item.placeholder"
			  v-bind="item.otherOptions"
			  :show-password="item.type === 'password'"
			  v-model="formData[`${item.field}`]"
			/>
    </el-form>
</template>

<script lang="ts">
import { defineComponent, PropType, ref, watch, computed } from 'vue'
export default defineComponent({
  props: {
    modelValue: {
      type: Object,
      required: true
    }
  },
  setup(props, { emit }) {
	//因为是浅拷贝,所以当改到formData.value.key,这里还是可以动态变化
    const formData = ref({ ...props.modelValue })
	
	  watch(
		  formData,
		  (newValue) => {
			console.log(newValue)
			emit('update:modelValue', newValue)
		  },
		  {
			deep: true
		  }
		)
    return {
      formData
    }
  }
})
</script>
+ 父组件
<div class="page-search">
<hy-form v-bind="searchFormConfig" v-model="formData">
  <template #header>
	<h1 class="header">高级检索</h1>
  </template>
  <template #footer>
	<div class="handle-btns">
	  <el-button icon="el-icon-refresh" @click="handleResetClick"
		>重置</el-button
	  >
	  <el-button type="primary" icon="el-icon-search">搜索</el-button>
	</div>
  </template>
</hy-form>
</div>
const formOriginData: any = {}
for (const item of formItems) {
  formOriginData[item.field] = ''
}
//父组件接收到重置表单的命令,清空原表单的数据
const handleResetClick = () => {
  for (const key in formOriginData) {
	formData.value[`${key}`] = formOriginData[key]
  }
}
  • 另一种模式
<!-- 父组件 -->
<template>
  <div class="page-search">
    <hy-form v-bind="searchFormConfig" v-model="formData">
      <template #footer>
        <div class="handle-btns">
          <el-button icon="el-icon-refresh" @click="handleResetClick"
            >重置</el-button
          >
        </div>
      </template>
    </hy-form>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import HyForm from '@/base-ui/form'

export default defineComponent({
  props: {
    searchFormConfig: {
      type: Object,
      reuqired: true
    }
  },
  components: {
    HyForm
  },
  emits: ['resetBtnClick', 'queryBtnClick'],
  setup(props, { emit }) {
    // 双向绑定的属性应该是由配置文件的field来决定
    // 1.优化一: formData中的属性应该动态来决定
    const formItems = props.searchFormConfig?.formItems ?? []
    const formOriginData: any = {}
    for (const item of formItems) {
      formOriginData[item.field] = ''
    }
    const formData = ref(formOriginData)

    // 2.优化二: 当用户点击重置
    const handleResetClick = () => {
      formData.value = formOriginData
     
    }

    return {
      formData,
      handleResetClick,
      handleQueryClick
    }
  }
})
</script>
  • 子组件使用modelValue
<template>
  <div class="hy-form">

    <el-form :label-width="labelWidth">
			<el-input
			  :placeholder="item.placeholder"
			  v-bind="item.otherOptions"
			  :show-password="item.type === 'password'"
			  :model-value="modelValue[`${item.field}`]"
			  @update:modelValue="handleValueChange($event, item.field)"
			/>
    </el-form>

  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  props: {
    modelValue: {
      type: Object,
      required: true
    },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const handleValueChange = (value: any, field: string) => {
      emit('update:modelValue', { ...props.modelValue, [field]: value })
    }

    return {
      handleValueChange
    }
  }
})
</script>
最后更新: 9/14/2022, 7:43:01 AM