一.声明式API编程
- 组合式函数编程
使用<script setup lang="ts"></scriopt>语法糖可以省去export default defineComponent
函数式编程:抽取一个可复用的日期格式化函数时。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出 。隐藏了内部实现,直接获取想要的值。
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
- 多个组合式函数的复用
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
//目标dom 事件 回调函数
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}
二.常用钩子函数
- computed计算属性
响应式或者其他数据在computed里面改变后,监听数据也会改变
const formlistruleoption = computed(() => {
const selectedOption = typeValueOptions.formList.find((i) => i.value === state.formListValue);
return selectedOption ? [selectedOption.rule, selectedOption.option] : [null, null];
});
- props
const props = defineProps<{ book: Book }>()
<script setup lang="ts">
const props = defineProps({
foo: { type: String, required: true },
bar: Number
})
props.foo // string
props.bar // number | undefined
</script>
defineProps实现的是运行时声明,因为defineProps内部传递的参数在运行时会调用
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
解构赋值props
- watch的用法
watch(oldvalue,(newvalue)=>{
})
watch(
() => state.formListValue, // 要监听的变量或对象
(value, oldValue) => { // 回调函数,value 和 oldValue 分别表示变化后和变化前的值
console.log('值变化-------??', value);
typeValueOptions.formList.forEach((i) => {
if (i.value == value) {
state.rule = i.rule
state.option = i.option
}
})
console.log('值变化-------???', state);
},
{
deep: true, // 是否深度监听对象内部变化(默认为 false)
immediate: true, // 是否在初始绑定时立即运行回调(默认为 false)
flush: 'post', // 变化时机是否延迟推迟到下一次主线任务执行之后(默认为 'pre')
// ... 其他选项参数
}
);
- watch和watchEffect的区别
watch是惰性执行,只有监听的数据值发生变化时才会执行,需要监听传递的对象;watchEffect不是,每次代码加载watchEffect都会执行,不需要监听传递的对象,会自动追踪函数内部使用的所有的响应式数据。
const state = reactive({
count:0,
name:'tom'
})
watch(state,()=>{
console.log(`Count is ${state.count}`)
})
watchEffect(() => {
console.log(`Count is ${state.count}`)
console.log(`Name is ${state.name}`)
})
- inject和provide的用法和区别
inject 接收数据 , 可以从任何父组件接收数据,数据可以是对象或者是基本数据类型
provide 分发数据 , 父组件向所有子组件分发数据,数据同样可以使对象或者基本数据类型,这两者的使用会让父子传值更加快捷,在封装组件时更加灵活
三.响应式对象
ref和reactive
对比与vue2,vue3使用ref和reactive代理对象生成响应式数据,目的是实现按需地对数据进行加载,前者是可以接受任何值类型,后者只适用于对象 (包括数组和内置类如
Map
和Set
)
- ref的使用和常用场景
<script setup>
import { ref } from 'vue'
// 给每个 todo 对象一个唯一的 id
let id = 0
const newTodo = ref('')
const todos = ref([
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
])
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value })
newTodo.value = ''
}
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
</script>
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>
实例中使用ref将字符串,对象数组装变成响应式数据类型,访问内容的时候需要用.value
当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用
.value
顶层属性的意思就是object,不是object.foo
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性; 简言之,
ref()
让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到 组合函数 中。
const obj = {
foo: ref(1),
bar: ref(2)
}
// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)
// 仍然是响应式的
const { foo, bar } = obj
跟响应式对象不同,当 ref 作为响应式数组或像
Map
这种原生集合类型的元素被访问时,不会进行解包。
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
// 推导得到的类型:Ref<number | undefined> 支持泛型参数
const n = ref<number>()
当ref被嵌套时,会自动解包,当新的ref给旧的赋值时,会被替换掉
- reactive的使用和常用场景
仅对对象类型有效(对象、数组和
Map
、Set
这样的集合类型), Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失;
const state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
就是说,局部变量解构属性的时候,属性丢失响应性,但是原来的响应式对象不受影响
- toRef的使用场景
- emit使用方法
// 声明触发的事件
const emit = defineEmits(['response'])
// 带参数触发
emit('response', 'hello from child')
- unref的使用场景
尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
四.状态管理 (pinia)
pinia是Vue3中使用的状态管理工具,类似于Vue2中的vuex,但是区别在于它不再需要通过set函数去修改状态值,它只需要直接修改
import { defineStore } from 'pinia';
// 取名是唯一的
export const useFormListStore = defineStore('useFormList', {
state: () => ({
hasTableData: false
}),
getters: {
// 由state中数据派生
},
actions: {
}
});
import { useFormListStore } from '/@/store/modules/formList.ts'
console.log(store.hasTableData)
对ts响应式对象声明的泛型,对内部对象类型赋值
export interface IFormData {
/**
* 创建时间
*/
createTime?: Date;
/**
* 创建人
*/
createUser?: number;
/**
* 编号
*/
fromId?: number;
/**
* 表单名称
*/
fromName?: string;
/**
* 表单备注
*/
fromRemark?: string;
/**
* 1 删除
*/
isDelete?: string;
params?: { [key: string]: any };
/**
* 备注
*/
remark?: string;
searchValue?: string;
/**
* 更新时间
*/
updateTime?: Date;
/**
* 更新人
*/
updateUser?: number;
}
对象类型赋值和限制泛型的方法:
const obj = reactive({
data : {} as IFormData
})
obj.data.append('key',value)
五.路由跳转传参方式
- path+query
如下代码传递了键值都是groupId对应的参数
this.$router.push({
// path: '',
name: "DesignHello",
// query: {
// groupId
// },
params: {
groupId
}
});
- name+query或params
如上代码传递了键值都是groupId对应的参数,params在路由地址中不显示,query会显示,params通过route.params.groupId获取传递的值,query直接通过route.groupId获取传递的值
六.JeecgBoot3+Vue3的组件使用
1.BasicForm
- 监听某一项的数据变化
componentProps: ({ formModel, formActionType }) => {
console.log('业务对象表单的值', formModel, formActionType);
return {
dictCode: 'flow_business_type',
onChange: (value) => {
console.log('业务对象表单的值', value);
}
}
},
配置对应表单项的componentProps属性即可,返回值包含了所需要的更改事件和字典配置,以及表单属性和配置相关
- 监听某一项的数据值是否有输入
watch(form.field,(newvalue)=>{
console.log('22222222',newvalue)
})
- 校验某一项输入,通过判断是否在数组中
{
field:'userName'
, dynamicRules: ({ model, schema }) => {
const allusernames = toRaw(store.AlluserName);
// console.log('输入信息', model, store.AlluserName);
console.log('输入信息', schema);
return [{
required: true,
message: '请输入用户账号'
}, {
validator: (_, value) => {
const isexists = allusernames.includes(value);
return new Promise<void>((resolve, reject) => {
if (isexists) {
reject('该用户账号已存在');
} else {
resolve();
}
})
}
}];
}
}
- setFieldsValue()的用法
setFieldsValue({ nodeType, nodeName, signType, sendMessage })//如果field的命名和获取到的属性一致可以这样写
- 记一次render属性配置
render: ({ model, field }) => {
let value = model[field]
return h('span', {
onChange: (value: string) => {
model[field] = value;
},
}, NodeTypes1[value]);
},
// 渲染的值放在第三个参数中
如果是逗号拼接的字符串转数组,需要注意要在第三个参数配置要显示的项
render: ({ model, field }) => {
let str = model[field].toString()
let value = str.split(',');
console.log('渲染', value)
return h('div', { style: { display: 'inline-block' } }, [
...value.map((v) => {
return h(Tag, { style: { marginRight: '8px' } }, sendMessageEnums[v]);
})
]);
}
- 枚举类型的定义需要注意的问题
export enum sendMessageEnums {
phone = '电话',
email = '邮箱',
weixin = '微信'
}
最好是一边字符串,一边是常量或者字面量
- 转字符串的方法
let str = String(model[field]);
let str = `${model[field]}`;//es6模版
- 插槽的使用方法
{
field: 'nodeName',
component: 'Input',
label: '审核人',
componentProps: {
disabled: true,
bordered: false
},
slot: 'nodeName'}
//界面
<BasicForm>
<template #nodeName ></template>
</BasicForm>
2.BasicTable
如果需要自定义表格数据,可以通过BasicTable的data-source属性绑定响应式数据,然后需要绑定render(如果datasource变化)内置方法重新渲染表格,不这样会报错,应为初始化的时候datasource并没有值,具体事例如下;
//html
<BasicTable :canResize="false" ref="tableRef" :data-source="tableData" :columns="columns" rowKey="id"
:reload="getDatasource()">
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
// js
async function getDatasource() {
await getFormList().then((res) => {
tableData.value = res.data.data
}).catch((err) => {
console.log('err', err);
})
}
这样操作会反复加载渲染表格,不推荐,一般是在api上面配置请求函数
- 表格中slot插槽用法
//表格配置colums
export const columns: BasicColumn[] = [ {
title: '发起人',
dataIndex: 'subUserName',
width: 70,
slots: { customRender: 'subUserName' },
}]
// 表格内部
<BasicTable>
<template #subUserName="{ record }">
//可渲染内容
</tempolate>
</BasicTable>
- 常见的使用场景
配置表格的属性和配置项,实现列表的数据展示,以及需要注意的情况
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'startUser'">
<div style="display: flex; align-items: center;">
<img :src="record.startUser.alisa ? record.startUser.alisa : `src/assets/images/logo.png`"
style="width: 35px;height: 35px; margin-right: 20px;" />
<span>{{ record.startUser.name }}</span>
</div>
</template>
<template v-else-if="column.key === 'recordState'">
<span>
<a-tag v-if="record.recordState == `1`" color="blue"> 处理中</a-tag>
<a-tag v-if="record.recordState == `2`" color="yellow">正常完成</a-tag>
<a-tag v-if="record.recordState == `3`" color="red">拒绝结束</a-tag>
<!-- <a-tag v-if="record.recordState == `4`" color="yellow">已结束</a-tag> -->
</span>
</template>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
// 列表页面公共参数、方法
const { tableContext } = useListPage({
designScope: 'position-template',
tableProps: {
api: getFlowRecordList,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
rowKey: 'id',
beforeFetch: (params) => {
params.page = { pageNo: params.pageNo, pageSize: params.pageSize };
params.query = [{ columnName: 'flowName', value: '', whereType: 'eq' }];
return params;
},
actionColumn: {
width: 70,
fixed: 'right',
},
rowSelection: {
getCheckboxProps: (record) => ({
disabled: true, // 设置禁用状态
}),
}
},
});
- 自定义查询条件
/**
* 查询列表
* @param params
*/
export const getFlowRecordList = async (params) => {
console.log('params', params);
const data = {
query: [
{ flowName: params.flowName },
{ keyWords: params.keyWords },
{ handleUser: store.userInfo.userId ? store.userInfo.userId : '' }
],
page: {
current: params.pageNo,
size: params.pageSize
}
}
const { records } = await defHttp.post({ url: Api.list, data });
return records;
};
需要在参数接口里转化传递的参数
- 自定义渲染表格字典
// income_method是收款类别字典,可以替换成其他内置的字典
export const columns = [{
title: '收付款类别', dataIndex: 'subjectSign', width: 100,
customRender: ({ text }) => render.renderDict(text, 'income_method'),
}]
3.BasicModal
- Hooks函数式组件界面
顾名思义,函数钩子,它的本质是一种重要的代码组合机制,最开始是React提出,Hooks 提供了一种单一的、功能性的方式来处理共享逻辑,并有可能减少代码量,传递的参数一般是响应式对象
- 具体实际用例
const todos = ref([
{ id: 1, text: 'Learn Vue3' },
{ id: 2, text: 'Build an app' },
{ id: 3, text: 'Deploy to production' }
])
const newTodoText = ref('')
function addTodo() {
const newTodo = {
id: todos.value.length + 1,
text: newTodoText.value
}
todos.value.push(newTodo)
newTodoText.value = ''
}
addTodo函数是hooks函数不带参数传递
4.Description
{
field: 'incrFee',
label: '递增金额',
render: (curVal, data) => {
if( data.increasesType == '2') return `${curVal}元`;
return `无`;
},
},
DescItem的render传递两个参数,那项的值和单条数据的值
- ant-design-vue的样式修改(deep不生效后的处理)
需要准确找到需要修改样式的类名,而且需要保证原始样式没有!important
- 常用函数
str.split(',')//字符串转数组
arr.join(',')//数组转字符串
arr.filter(i=>i.id==id)//返回值是boolean会筛选值为true的项
arr.map(i=>{ let newItem = { ...i,j:'' } return newItem})//返回值不会影响原生数组,他创建了一个新的数组
arr.forEach(i => { i.j = '';});//这样处理会修改arr原始数组的数据
arr.splice(0,length-1)//截取数组除了最后一位的所有,取头不取尾arr.find(i=>i>3) //返回满足条件的第一项,不能返回数组,这是和filter最大的区别
- JeecgBoot中JSelectMultiple组件setFieldValues时不生效(尽量不适用这个,采用Select和mode:mutiple替代)
- 记一次封装部门用户选择,对应新系统接口(覆写JSelectUserByDept 组件,没有必要,直接替换官方接口就行,JSelectUser也可以单独使用)
4.用户选择组件(JSelectUser)
- 记一次单独使用组件经历
<a-button shape="round" @click="openHandle">选择</a-button>
<UserSelectModal rowKey="userId" @register="registerSelUserModal" @getSelectResult="onSelectOk" />
// 注册用户选择框
const [registerSelUserModal, { openModal: openUserModal, closeModal: closeUserModal }] = useModal()
// 打开用户选择框
function openHandle() {
openUserModal();
}
// 选择用户成功
function onSelectOk(options, values) {
formData.selected = values.join(',')
formData.showNickName = options[0].nickName
console.log('选择的用户', options, values, formData.selected)
closeUserModal()
//处理业务逻辑
}
七.Vue3的进阶使用方法
1.函数式组件的自定义和封装
<script setup lang='ts'>
import { ref, h } from "vue"
/**
* Implement a functional component :
* 1. Render the list elements (ul/li) with the list data
* 2. Change the list item text color to red when clicked.
*/
const ListComponent = (props) => {
console.log('属性',props)
return h('ul',props.list.map((o,i)=>{
return h('li',{
key:i,
onClick:()=>props.onToggle(i),//这里面函数需要加上on然后函数要首字母大写
style:props['active-index'] == i ? {color:'red'} : null,
},o.name)
}))
}
const list = [{
name: "John",
}, {
name: "Doe",
}, {
name: "Smith",
}]
const activeIndex = ref(0)
function toggle(index: number) {
activeIndex.value = index
}
</script>
<template>
<list-component
:list="list"
:active-index="activeIndex"
@toggle="toggle"
/>
</template>
2.对话框+表单模板
<template>
<BasicModal v-bind="$attrs" :title="modalTitle"
:height="500"
@register="register"
@ok="handleSubmit"
cancelText="取消"
okText="确认"
destroyOnClose >
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, useAttrs, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { TerMinationForm } from '../contract.data'
import { TerminationContract, editBatchStopDate } from '../contract.api'
const attrs = useAttrs();
const emit = defineEmits(['success'])
const modalTitle = ref('退租合同')
/**
* 下拉菜单触发key
*/
const key = ref('')
const billIds = ref('')
//自定义接受参数
const props = defineProps({
//是否禁 用页面
contractId: { type: String, default: '' },
});
//注册弹框
const [register, { closeModal }] = useModalInner( async (data) => {
console.log('表单值',data)
await resetFields();
})
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate, updateSchema, resetSchema }] = useForm({
schemas: TerMinationForm,
labelWidth: 100,
showResetButton:false,
showSubmitButton: false,
});
async function handleSubmit(){
const values = await validate();
console.log('表单值',values)
try {
let params = {
...values,
billIds: unref(billIds),
contractId: props.contractId,
}
await TerminationContract(params,closeModal());
emit('success')
;
} catch (error) {
}
}
</script>