jeecgboot vue TS 模板化 04

jeecgboot vue TS  模板化 04 ref / reactive / 函数script setup 是 Vue 3 的 语法糖 它让组合式 API 用起来更简洁。在 setup 里定义的变量和函数可以 直接在模板里用 不需要 return 。配合 TS它变成了编译期静态检查 运行时响应式的组合。这一节的所有例子都来自 jeecgboot-vue3/src 真实代码1. script setup 的三板斧 ref / reactive / 函数script setup langts import { ref, reactive, onMounted } from vue; import { useRouter } from vue-router; import { useMessage, useNotification } from naive-ui; import { useUserStore } from //store/modules/user; // ① loading 这种开关状态 → 用 refboolean const loading refboolean(false); // ② 表单对象 → 用 reactiveLoginParams const loginForm reactiveLoginParams({ username: , password: , captcha: , }); // ③ store / router / notification const userStore useUserStore(); const { notification } useMessage(); const router useRouter(); // ④ 异步函数try/catch/finally 的标准套路 async function handleLogin() { loading.value true; // ← script 里必须 .value try { const userInfo await userStore.login({ ...loginForm, captcha: formModelRef.value?.captcha || , rememberMe: formModelRef.value?.rememberMe || false, }); notification.success({ message: 登录成功, description: 欢迎回来${userInfo?.realname}, }); router.push(/home); } catch (e) { notification.error({ message: 登录失败, description: (e as Error).message, // ← catch 的 e 是 unknown需要断言 }); } finally { loading.value false; } } /script template !-- ⑤ 模板里直接用 loading / loginForm不需要 .value -- a-spin :spinningloading a-form :modelloginForm a-input v-model:valueloginForm.username placeholder账号 / a-input-password v-model:valueloginForm.password placeholder密码 / a-button typeprimary block :loadingloading clickhandleLogin 登录 /a-button /a-form /a-spin /templaterememberMe: formModelRef.value?.rememberMe || false里面的问号?.是可选链操作符Optional Chaining作用是安全访问对象的属性防止访问null或undefined时报错。具体行为formModelRef.value如果不是 null 或 undefined就会读取.rememberMe的值。如果formModelRef.value是null 或 undefined整个表达式formModelRef.value?.rememberMe返回undefined不会报错。后面的|| false保证当值是undefined或false时最终rememberMe至少会是false。问号?.用于安全地访问可能不存在的对象属性是 TypeScript/现代 JavaScript 的语法糖。description: 欢迎回来${userInfo?.realname},如果userInfo是null或undefined那么userInfo?.realname会返回undefined而不会报错。refT 的最佳实践 // ✅ 推荐显式写泛型TS 不会推断错 const loading refboolean(false); const tableData refDepartItem[]([]); // ❌ 不推荐空数组会被推断为 never[] const tableData ref([]); // 类型是 never[]后面 push 什么都会类型报错 // ✅ 推荐ref 里放对象也可以但要注意在脚本里用 .value const currentRecord refDepartItem | null(null); console.log(currentRecord.value?.departName); // 可选链null 时不报错// ✅ 推荐对象用 reactive const form reactiveLoginParams({ username: , password: }); // ❌ 不要把基本类型塞 reactive const state reactive({ loading: false }); // 能用但没必要直接 ref 更简洁数据类型推荐写法原因表格数据列表refT[]([])接口返回后经常整体替换当前选中行refT()经常整体赋值表格查询条件reactive({})属性频繁修改分页参数reactive({})pageNo、pageSize 经常单独改表单数据reactive({})字段多双向绑定方便// 标量 const loading ref(false) // 表格数据 const dataSource refUser[]([]) // 当前记录 const currentRecord refUser() // 查询条件 const searchForm reactive({}) // 编辑表单 const formData reactive({}) // 分页 const pagination reactive({})definePropsTscript setup 里用 defineProps 声明组件 props。带 TS 的版本和不带 TS 的版本写法差别非常大MyComp :listdataList :loadingisLoading /const props defineProps({ list: { type: Array, default: () [], // 如果父组件没传 list就默认是空数组 }, loading: { type: Boolean, default: false, // 如果父组件没传 loading就默认 false }, });作用是给组件外部传入的数据做类型和默认值校验父组件 → 子组件的数据传递propsscript setup const emit defineEmits([change, submit]) function handleClick() { emit(change, 123) // 传值给父组件 } /script template button clickhandleClick点击/button /templateMyComp changehandleChange submithandleSubmit / function handleChange(val) { console.log(子传来的值:, val) }script setup langts import { defineProps, withDefaults } from vue; import { DepartItem } from //api/sys/model/departModel; // ① 最基本只声明类型不设默认值 const props defineProps{ list: DepartItem[]; loading?: boolean; }(); // ② 需要默认值 → 用 withDefaults 包一层 const props withDefaults(defineProps{ list: DepartItem[]; loading?: boolean; columns: number; }(), { loading: false, columns: 3, }); /script- defineProps{ ... }() 的尖括号里写的是 对象字面量类型 它看起来像 JS 对象但本质是 TS 类型语法- ? 表示可选父组件可以不传- withDefaults 是给类型默认值的标准写法 必须和 definePropsT 配对使用defineEmitsT —— 组件事件的类型写法defineEmits 声明子组件能触发哪些事件以及事件回调的参数类型script setup langts import { defineEmits } from vue; // 声明事件名 回调函数签名 const emit defineEmits{ (e: update, row: DepartItem): void; (e: delete, id: string): void; (e: close): void; }(); // 触发事件 emit(update, currentRow.value); // ✅ TS 会检查 currentRow 的类型是否匹配 emit(delete, row.id); // ✅ emit(close); // ✅ /scripttemplate a-button clickemit(delete, record.id)删除/a-button a-button clickemit(update, record)编辑/a-button /template为什么带类型的 emit 很重要 没有类型约束时 emit(updat, row) 这种手误只能在运行时发现。有了类型TS 会在编译期就给你亮红v-for TS模板里的类型推断v-for 有一个非常强的特性 只要你在 script 里给数组写了类型模板里 item 的类型就会自动推断 。script setup langts import { ref } from vue; import { departListApi } from //api/sys/depart; import { DepartItem } from //api/sys/model/departModel; // 显式声明数组的元素类型 const tableData refDepartItem[]([]); // 页面加载 onMounted(async () { tableData.value await departListApi({ page: 1, pageSize: 10 }); }); /script template !-- ✅ 这里 item 自动被推断为 DepartItem -- !-- 在模板里敲 item. 会出 departName / orgCode / createTime 的字段提示 -- a-table :data-sourcetableData a-table-column v-forcol in columns :keycol.key :titlecol.title :data-indexcol.key template #bodyCell{ record } !-- record 在这里是 any因为 Ant Design Vue 的表格 record 是动态的 -- !-- 如果需要强类型在模板里用 record as DepartItem 也行 -- span{{ record.departName }}/span /template /a-table-column /a-table !-- 直接 v-for 一个带类型的数组 → item 自动推断为 DepartItem -- div v-foritem in tableData :keyitem.id p{{ item.departName }}/p p{{ item.orgCode }}/p /div /template- v-for 不能在模板里写类型标注 比如 v-for(item: DepartItem) in list 是 语法错误 你项目里如果有这样的代码要删掉冒号后面的类型- 类型约束应该放在 script 里的数组声明上 refDepartItem[]([]) 就够了- 模板里的 item 会自动推断组合式函数Composable)JeecgBoot 里已经把最常用的页面逻辑抽成了 useXxx 形式的 composable。这是 script setup 时代最强大的代码复用方式。看你项目里典型的 useListPage src/hooks/system/useListPage.ts 它的 TS 用法就是一个非常好的范例export function useListPage( options: UseListPageOptions // 函数参数 ):///这是函数的返回值类型声明。意思是 useListPage 返回一个对象对象里面有这些属 { dataSource: Refany[]; loading: Refboolean; pagination: RefPagination; searchForm: any; loadData: (params?: Recordable) Promisevoid → 函数 params?: Recordable 是参数可选类型是一个任意键值对象 Promisevoid 表示函数返回一个 Promise最终不返回数据void handleTableChange: (pag: Pagination) void; handleReset: () void; handleSizeChange: (size: number) void; } { // 内部实现统一处理表格的加载、分页、搜索条件重置 } 输入参数options比如分页配置、接口函数等 输出一个 对象对象里包含你要在组件里用的状态和方法const { dataSource, loading, loadData } useListPage({ pageSize: 10, api: fetchList });页面里用它script setup langts import { useListPage } from //hooks/system/useListPage; const { tableProps, // a-table 的全部 propsdataSource、pagination、loading、columns... searchFormSchema, // 搜索表单的 schema onLoadData, // 触发重新加载 } useListPage({ tableProps: { title: 部门列表, columns, actionColumn, api: departListApi, }, searchInfo: { labelWidth: 100, schemas: searchFormSchema, }, }); /script hook 内部会 根据传的 api 拉取数据 自动处理分页、加载状态 把 tableProps 和搜索表单 schema 封装好返回 提供 onLoadData() 用来刷新表格数据 template !-- 搜索区 -- jeecg-search-form searchonLoadData resetonLoadData :label-widthsearchForm.labelWidth a-form-item v-foritem in searchFormSchema :keyitem.field :labelitem.label !-- 根据 item.component 渲染不同输入框 -- /a-form-item /jeecg-search-form 使用 searchFormSchema 渲染表单字段 search 和 reset 都会触发 onLoadData()重新拉取数据 label-width 控制表单 label 宽度 !-- 表格区 -- jeecg-table v-bindtableProps / tableProps 是 Hook 返回的完整表格配置 包含数据源、分页信息、加载状态、列定义、操作列等 v-bindtableProps 是把对象里的所有属性直接绑定到组件上 /template- 表格逻辑被抽成一个函数 100 个列表页都只需要 useListPage({...}) 一行- TS 类型让 tableProps.columns 的每个字段都有提示 减少手误- 搜索 表格 分页三件套 完全解耦不同模块之间复制粘贴成本极低自写hookHookuseCounter最经典入门版// useCounter.ts import { ref } from vue; export function useCounter(initial 0) { // 状态 const count ref(initial); // 方法加1 const increment () { count.value; }; // 方法减1 const decrement () { count.value--; }; // 方法重置 const reset () { count.value initial; }; // 返回“状态 方法” return { count, increment, decrement, reset, }; }script setup langts import { useCounter } from ./useCounter; const { count, increment, decrement, reset } useCounter(10); /script template div当前值{{ count }}/div button clickincrement1/button button clickdecrement-1/button button clickreset重置/button /template不用 Hook用 Hook每个组件都写 count 方法直接useCounter()代码重复逻辑复用分散集中管理没错你看到的本质就是封装对象。在 Vue 3 / React / 前端世界里“Hook” 的概念其实就是把状态 操作逻辑封装成一个函数然后返回给组件使用。它和普通函数的区别不是返回值类型而是用途和约定// 普通函数 function add(a: number, b: number) { return a b; } // Hook 风格 function useCounter() { const count ref(0) const increment () count.value return { count, increment } }普通函数只返回计算结果Hook 返回状态 操作并且状态是响应式的Hook 可以被组件直接绑定到模板并随组件更新script setup TS 的 5 个高频坑ref 在 script 里必须 .value 在 template 里不用script setup langts const count ref(0); console.log(count); // ❌ 打印的是 Ref 对象不是 0 console.log(count.value); // ✅ 要拿值必须 .value function inc() { count.value; // ✅ script 里赋值要 .value } /script template p{{ count }}/p !-- ✅ template 自动解包不用 .value -- a-button clickcount1/a-button !-- ✅ template 里自增也不用 .value -- /template坑 2 reactive 的整体赋值会丢失响应式const state reactiveDepartItem({ id: , departName: , orgCode: }); // ❌ 不要这么写整体重新赋值会丢响应式 state { id: 1, departName: 新部门 }; // ✅ 方案 A改用 ref const state refDepartItem({ id: , departName: , orgCode: }); state.value { id: 1, departName: 新部门 }; // OK // ✅ 方案 BObject.assign Object.assign(state, { id: 1, departName: 新部门 }); // OK坑 3 catch (e) 里的 e 是 unknown 不能直接读属性try { await userStore.login(params); } catch (e) { // ❌ e 是 unknown直接读 .message TS 会报错 // notification.error({ message: e.message }); // notification.error({ message: e?.message ?? 请求失败, }); // ✅ 先断言成 Error const err e as Error; notification.error({ message: err.message }); // ✅ 更稳妥的写法防御运行时 null if (e instanceof Error) { notification.error({ message: e.message }); } }写法含义e.message直接访问可能报错e?.message安全访问推荐e?.message ?? xxx有默认兜底最佳实践坑 4模板里不能写 v-for(item: DepartItem) in list!-- ❌ 语法错误模板里不能写 : DepartItem -- div v-for(item: DepartItem) in list :keyitem.id{{ item.name }}/div !-- ✅ 正确写法类型在 script 里标注 -- script setup langts const list refDepartItem[]([]); /script template div v-foritem in list :keyitem.id{{ item.departName }}/div /template坑 5 v-model 的可选链在模板里有限制!-- ✅ v-model 不能用可选链要确保绑定的对象存在 -- a-input v-model:valueformModelRef.value.username / !-- ✅ -- a-input v-model:valueformModelRef?.username / !-- ❌ 编译错误 --const formModelRef ref({ username: })实际结构是formModelRef ↓ { value: { username: } }所以写法含义formModelRef.value拿到真实对象formModelRef.value.username访问字段formModelRef是 ref → 没有 username真正数据在.value里面?.不能参与“赋值链路”v-model 需要 set✅ script 里必须 .valueconsole.log(userInfo.value?.realname)❌ script 里不能省console.log(userInfo.realname) // 错✅ template 里自动解包p{{ userInfo.realname }}/pscript setup langts import { ref } from vue; import { GetUserInfoModel } from //api/sys/model/userModel; const userInfo refGetUserInfoModel | null(null); /script template !-- ✅ 推荐用 v-if 先判空再读属性 -- div v-ifuserInfo p{{ userInfo.realname }}/p p{{ userInfo.username }}/p /div !-- ❌ 模板里可以用可选链但配合 v-if 更清晰 -- div{{ userInfo?.realname }}/div /template div {{ userInfo?.value?.realname }} /div表达式检查谁是否有效userInfo?.realnameref 对象❌ 无意义userInfo?.value?.realnamevalue✅ 正确动态组件 component :isxxx 与 TSscript setup langts import { ref, computed } from vue; import FormA from ./FormA.vue; import FormB from ./FormB.vue; // 用联合类型约束当前组件 const currentTab refa | b(a); // 用 computed 动态返回组件 const CurrentComponent computed(() { switch (currentTab.value a ? FormA : FormB); }); /script template !-- 动态渲染组件用 :is 绑定 -- component :isCurrentComponent / /templatev-model 不能用可选链!-- ❌ 编译错误v-model 不支持可选链 -- a-input v-model:valueformModelRef?.username / !-- ✅ 正确写法保证对象肯定存在 -- a-input v-model:valueformModelRef.value.username /?.在 Vue 里是有效的 JS但 Vue 的“ref 自动解包”不会和?.自动组合所以你必须明确.value才能让可选链真正作用在数据上- refT 存标量和数组 reactiveT 存表单/搜索条件- definePropsT withDefaults 是 Vue 3 组件声明 props 的标准写法- defineEmitsT 给事件回调加类型杜绝拼写错误- v-for 的 item 类型自动推断 但模板里不能写 (item: Type) 类型要写到 script 里的数组声明- catch 里的 e 是 unknown 要先 as Error 才能读 .message- reactive 不能整体重新赋值 要么改用 ref 要么用 Object.assign- Composables useXxx 是 Vue 3 时代最核心的代码复用方式JeecgBoot 的 useListPage 就是绝佳范例