Day02——基础数据开发-服务管理前端

目录

1 目标

各位同学,我们在第一天中,已经完成了服务管理下护理项目的后端接口开发,并且我们也做了测试。但是光有接口是不行的,还需要有前端的页面效果,那今天呢,我们就来开发护理项目的前端部分

开发这个需要大家具备VUE3 + TS 的基础才行。

我们今天主要的目标是:

  • 使用前端开发的视角来重新分析需求,掌握其分析套路和策略
  • 掌握TDesign组件选择使用的方式
  • 能够动手完成护理项目页面开发
  • 能够独立完成前端与后端的接口对接和联调

2 护理项目-设计

我们再来回顾一下前端的开发流程,大家来看这个图

在这里插入图片描述


在功能实现之前,我们有很多的工作需要完成,而这就是设计这部分的内容,主要包含了需求分析评估工期接口设计

其中到目前为止,后端已经提供了在线接口文档Knife4j,现在需要做的就是需求分析和功能实现

评估工期,我们在学习阶段不太关注,先跳过

在day01中我们已经分析过需求了,为什么还要做需求分析呢?

因为前端和后端对于需求来说,关注的点是不一样的,后端更注重逻辑,前端更注重展示效果,并且在完成功能之前,我们还需要查阅UI设计师提供的设计稿(图)来辅助我们完成页面的开发

请查看当天的资料:UI设计

2.1 页面分析

下面,我们来重新分析一下需求,还是打开原型图,找到护理项目这个小模块

列表页

在这里插入图片描述


当点击新增护理项目按钮或者是列表项中编辑的时候,需要弹窗进行新增或者是编辑,如下图

在这里插入图片描述
  • 点击【新增护理项目】,出【新增护理项目】弹窗
  • 点击编辑,出【编辑护理项目】弹窗

当编辑输入框内容的时候,验证规则如下:

在这里插入图片描述
关于价格、排序、图片这三个字段,需要进一步查看公共说明文档

在列表项操作中的删除禁用也会有弹窗,大家可以打开原型的全局/公共说明

删除弹窗

在这里插入图片描述


禁用弹窗

在这里插入图片描述
注意:启用与禁用操作、逻辑相反,且不出确认弹窗;

通过以上分析,我们现在大概知道了,这个护理项目分为了四个部分,分别是:

  • 列表页
  • 新增和编辑弹窗
  • 删除弹窗
  • 禁用弹窗

以上所有的页面,我们都可以到TDesign组件去寻找,来适配我们当前的项目

2.2 页面效果

前端在做开发的时候,我们开发的效果,要完全还原UI的设计稿效果,这个也属于验收测试的一部分,所以在页面开发的时候,UI设计稿也是我们需要先分析和查看的。下面是一些页面的配色、布局、样式和要求

弹窗模板

在这里插入图片描述

列表页

在这里插入图片描述
在这里插入图片描述

3 护理项目-功能开发

现在我们再来回顾一下,前端主要包含的技术:vue3 + typescript + TDesign + vite + pinia

其中TDesign组件我们开发页面重要参考,这里面提供了非常多的组件,大家需要收藏这个官网地址,方便我们随时查看:https://tdesign.tencent.com/vue-next/overview

当我们已经完成了需求分析,开发具体功能基本的步骤有四个

  • 先到TDesign组件找到对应的组件,先用静态组件在项目中展示效果
  • 编写接口代码,参考knife4j在线接口文档
  • 修改静态页面,调用接口,动态渲染数据
  • 样式微调,公共组件封装
疑问:没有在线接口文档怎么办?先到TDesign组件找到对应的组件,先用静态组件在项目中展示效果mock接口数据(模拟接口和数据) | 离线的接口文档(前后端共同制定)修改静态页面,调用mock接口,渲染数据样式微调,公共组件封装
两者对比:在后期接口联调的时候,mock接口的成本更高

3.1 护理项目-分页条件查询列表

3.1.1 静态组件

@/pages/serve/plan/project/目录中新建index.vue组件,我们现在想要一个列表分页展示,可以参考TDesign组件提供好的基础内容,链接如下:https://tdesign.tencent.com/vue-next/components/table

我们找到基础表格

在这里插入图片描述

然后拷贝它所有的代码,到我们创建的index.vue组件中

<template><t-spacedirection="vertical"><!-- 按钮操作区域 --><t-radio-groupv-model="size"variant="default-filled"><t-radio-buttonvalue="small">小尺寸</t-radio-button><t-radio-buttonvalue="medium">中尺寸</t-radio-button><t-radio-buttonvalue="large">大尺寸</t-radio-button></t-radio-group><t-space><t-checkboxv-model="stripe"> 显示斑马纹 </t-checkbox><t-checkboxv-model="bordered"> 显示表格边框 </t-checkbox><t-checkboxv-model="hover"> 显示悬浮效果 </t-checkbox><t-checkboxv-model="tableLayout"> 宽度自适应 </t-checkbox><t-checkboxv-model="showHeader"> 显示表头 </t-checkbox></t-space><!-- 当数据为空需要占位时,会显示 cellEmptyContent --><t-tablerow-key="index":data="data":columns="columns":stripe="stripe":bordered="bordered":hover="hover":table-layout="tableLayout ? 'auto' : 'fixed'":size="size":pagination="pagination":show-header="showHeader"cell-empty-content="-"resizable@row-click="handleRowClick"></t-table></t-space></template><scriptsetuplang="jsx">import{ ref }from'vue';import{ ErrorCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon }from'tdesign-icons-vue-next';const statusNameListMap ={0:{label:'审批通过',theme:'success',icon:<CheckCircleFilledIcon />},1:{label:'审批失败',theme:'danger',icon:<CloseCircleFilledIcon />},2:{label:'审批过期',theme:'warning',icon:<ErrorCircleFilledIcon />},};const data =[];const total =28;for(let i =0; i < total; i++){ data.push({index: i +1,applicant:['贾明','张三','王芳'][i %3],status: i %3,channel:['电子签署','纸质签署','纸质签署'][i %3],detail:{email:['[email protected]','[email protected]','[email protected]'][i %3],},matters:['宣传物料制作费用','algolia 服务报销','相关周边制作费','激励奖品快递费'][i %4],time:[2,3,1,4][i %4],createTime:['2022-01-01','2022-02-01','2022-03-01','2022-04-01','2022-05-01'][i %4],});}const stripe =ref(true);const bordered =ref(true);const hover =ref(false);const tableLayout =ref(false);const size =ref('medium');const showHeader =ref(true);const columns =ref([{colKey:'applicant',title:'申请人',width:'100'},{colKey:'status',title:'申请状态',cell:(h,{ row })=>{return(<t-tag shape="round" theme={statusNameListMap[row.status].theme} variant="light-outline">{statusNameListMap[row.status].icon}{statusNameListMap[row.status].label}</t-tag>);},},{colKey:'channel',title:'签署方式'},{colKey:'detail.email',title:'邮箱地址',ellipsis:true},{colKey:'createTime',title:'申请时间'},]);consthandleRowClick=(e)=>{ console.log(e);};const pagination ={defaultCurrent:1,defaultPageSize:5, total,};</script>

在上面的代码中,主要控制数据显示的是<t-table>标签,我们来详细说一下这个标签的内容

<t-tablerow-key="index":data="data":columns="columns":stripe="stripe":bordered="bordered":hover="hover":table-layout="tableLayout ? 'auto' : 'fixed'":size="size":pagination="pagination":show-header="showHeader"cell-empty-content="-"resizable@row-click="handleRowClick">
  • row-key
  • :data 数据源 (数组)
  • :columns 列配置(数组)
  • :stripe 是否显示斑马纹
  • :bordered 是否显示表格边框
  • :hover 是否显示鼠标悬浮状态
  • :table-layout 表格布局方式 可选项:auto/fixed
  • :size 表格尺寸
  • :pagination 分页配置, 用于模块内切换内容的控件
  • :show-header 是否显示表头
  • cell-empty-content 单元格数据为空时呈现的内容
  • resizable 是否允许调整列宽
  • @row-click 行点击时触发,泛型 T 指表格数据类型

table组件更多的api参考:https://tdesign.tencent.com/vue-next/components/table?tab=api

我们现在启动前端项目,同时也需要启动后端,然后在网页上访问服务管理–>护理计划–>护理项目

如果能看到如下效果,就证明刚才的路由和vue创建都是成功的

在这里插入图片描述

3.1.2 查询接口

定义模型对象:在src/api/model中新增一个文件serveModel.ts,代码如下

exportinterfaceProjecListModel{ createBy:number createTime:string creator:string id:number image:string name:string nursingRequirement:string orderNo:number price:number status:number unit:string updateBy:number updateTime:string}
以上类型定义需要与后台接口字段和类型进行对应,参考在线接口文档

在src/api目录中新增一个serve.ts文件,定义接口,代码如下:

import{ request }from'@/utils/request'importtype{ ProjecListModel }from'@/api/model/nursingModel'// 分页查询护理项目信息exportfunctiongetProjectList(params){return request.get<ProjecListModel>({ url:`/nursing_project`, params })}
注意:路径要与后台提供的接口路径保持一致,参考在线接口文档

3.1.3 修改静态页面,调用接口,动态渲染数据

3.1.3.1 定义表头

我们查看原型,表格中的表格如下:

在这里插入图片描述


<t-table>标签中有一个属性叫做:columns,它是一个数组,里面可以定义表头

为了方便管理,我们在@/pages/serve/plan/project/文件夹下创建一个constants.ts文件

exportconstCOLUMNS=[{ title:'序号', align:'left', width:100, minWidth:100, colKey:'rowIndex'},{ title:'护理图片', width:150, minWidth:'150px', colKey:'image'},{ title:'护理项目名称', minWidth:'150px', colKey:'name'},{ title:'价格(元)', minWidth:'160px', colKey:'price'},{ title:'单位', minWidth:'150px', colKey:'unit'},{ title:'排序', minWidth:'150px', colKey:'orderNo'},{ title:'创建人', minWidth:'200px', colKey:'creator'},{ title:'创建时间', minWidth:'180px', colKey:'createTime'},{ title:'状态', colKey:'status', width:120, minWidth:'120px',cell:(h,{ row })=>{const statusList ={1:{ label:'启用'},0:{ label:'禁用'}}returnh('span',{class:`status-dot status-dot-${row.status}`}, statusList[row.status].label )}},{ align:'left', fixed:'right', width:154, minWidth:'154px', colKey:'op', title:'操作'}]

然后在index.vue中删除掉关于columns定义的属性,然后引入我们刚才定义的constants.ts文件

import{COLUMNS}from'./constants'

修改<t-table :columns="COLUMNS"> 让标签中的:columns指向我们引入的COLUMNS

3.1.3.2 静态数据展示

我们打开knife4j在线接口文档,找到护理项目接口,做一个测试,获得到json相关的数据

在这里插入图片描述

获取json之后,我们可以修改index.vue中的for循环,模拟真实的接口数据

const total =16for(let i =0; i < total; i++){ data.push({index: i +1,createTime:'2023-07-11 15:24:52',updateTime:'2023-07-18 14:41:51',creator:'张三李四王五',name:'喂饭',orderNo:1,unit:'次',price:20,image:'https://yjy-slwl-oss.oss-cn-hangzhou.aliyuncs.com/2a6ababd-6f0d-4e07-9dff-ca34123912dc.png',status:1,count:0})}

保存查看效果如下:

在这里插入图片描述


这样就可以展示真实的数据了

3.1.3.3 添加样式

目前前端开发人员已经对页面样式进行了修复,在table标签外部,添加以下三个div,来修饰样式

<template><divclass="min-h serveProject bg-wt"><divclass="baseList"><divclass="tableBoxs"><t-tabel><!-- 其他代码省略 --></t-tabel></div></div></div></template>
3.1.3.4 图片预览处理

目前在列表中展示的是图片的路径,我们的需求是,需要展示小图,并且可以预览图片(大图)
这个在TDesign组件中已经提供了
网址:https://tdesign.tencent.com/vue-next/components/image-viewer
我们在<t-table></t-table>标签对内处理图片的展示,代码如下:

<t-table><!-- 图片预览及展示 --><template#image="{ row }"><divclass="tdesign-demo-image-viewer__base"><t-image-viewer:images="[row.image]"><template#trigger="{ open }"><divclass="tdesign-demo-image-viewer__ui-image"><imgalt="test":src="row.image"class="tdesign-demo-image-viewer__ui-image--img"/><divclass="tdesign-demo-image-viewer__ui-image--hover"@click="open"><span><BrowseIconsize="1.4em"/> 预览</span></div></div></template></t-image-viewer></div></template></t-table>
3.1.3.5 价格展示

在列表展示的价格需要保留小数点后两位,如果是整数,需要补零
我们同样在<t-table></t-table>标签对内处理字段的展示,代码如下:

<t-table><!-- 价格拼接 --><template#price="{ row }"> {{ isDecimals(row.price) ? row.price : row.price + '.00' }} </template></t-table><scriptsetuplang="ts">//在js代码中添加isDecimals方法,判断当前字段是否包含了小数点constisDecimals=(val)=>{if(String(val).indexOf('.')>-1){returntrue}returnfalse}</script>
3.1.3.6 操作按钮处理

在table的最右侧,有三个按钮,我们先来看效果

在这里插入图片描述


分别代表了不同的操作,我们目前先处理显示的问题,后期我们再做具体的操作

我们同样在<t-table></t-table>标签对内处理字段的展示,代码如下:

<t-table><!-- 操作栏 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl">删除</a><aclass="font-bt">编辑</a><aclass="delete">禁用</a></div></template></t-table>
标签中的样式,在提供的前端工程中已经包含,大家直接使用即可
3.1.3.7 调用接口,并调式分页组件

把刚才我们定义的接口先引入到index.vue中,然后编写调用接口的方法,代码如下:

<template><!-- 当数据为空需要占位时,会显示 cellEmptyContent --><t-table:row-key="rowKey":data="data":columns="COLUMNS"vertical-align="middle":hover="hover":loading="dataLoading"tabel-content-width="100%"table-layout="fixed"><!-- 价格拼接 --><template#price="{ row }"> {{ isDecimals(row.price) ? row.price : row.price + '.00' }} </template><!-- 图片预览及展示 --><template#image="{ row }"><divclass="tdesign-demo-image-viewer__base"><t-image-viewer:images="[row.image]"><template#trigger="{ open }"><divclass="tdesign-demo-image-viewer__ui-image"><imgalt="test":src="row.image"class="tdesign-demo-image-viewer__ui-image--img"/><divclass="tdesign-demo-image-viewer__ui-image--hover"@click="open"><span><BrowseIconsize="1.4em"/> 预览</span></div></div></template></t-image-viewer></div></template><!-- 操作栏 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl">删除</a><aclass="font-bt">编辑</a><aclass="delete">禁用</a></div></template></t-table><!-- 分页 --><t-paginationv-if="total > 10"v-model="pagination.pageNum"v-model:pageSize="pagination.pageSize":total="total"@change="onPageChange"/></template><scriptsetuplang="ts">import{ onMounted, ref }from'vue'import{COLUMNS}from'./constants'import{ getProjectList }from'@/api/serve'var data =ref([])var total =ref(0)const dataLoading =ref(false)// 加载中//分页对象const pagination =ref({pageSize:10,pageNum:1})//生命周期onMounted(()=>{getList()})//获取列表数据constgetList=async()=>{const res =awaitgetProjectList(pagination.value) data.value = res.data.records total.value =Number(res.data.total)}// 翻页设置当前页constonPageChange=(val)=>{ pagination.value.pageNum = val.current pagination.value.pageSize = val.pageSize getList()}constisDecimals=(val)=>{if(String(val).indexOf('.')>-1){returntrue}returnfalse}</script>

我们现在,可以刷新网页,就可以看到跟原型图和UI设计稿几乎一样的效果了

在这里插入图片描述
3.1.3.8 序号处理

但是我们发现,在上述的效果展示中,没有序号了

因为我们之前在使用for循环模拟数据的时候是给了设置了一个index字段的,但是我们查询的接口中并没有这个字段,现在我们可以使用前端TDesign中的表格属性rowIndex解决,代码如下:

我们同样在<t-table></t-table>标签对内处理字段的展示,代码如下:

<template><t-table><!-- 序号 --><template#rowIndex="{ rowIndex }">{{ rowIndex + 1 }}</template></t-table></template>

效果如下:

在这里插入图片描述


最终代码:

<template><divclass="min-h serveProject bg-wt"><divclass="baseList"><divclass="tableBoxs"><!-- 当数据为空需要占位时,会显示 cellEmptyContent --><t-table:row-key="rowKey":data="data":columns="COLUMNS"vertical-align="middle":hover="hover":loading="dataLoading"tabel-content-width="100%"table-layout="fixed"><!-- 序号 --><template#rowIndex="{ rowIndex }">{{ rowIndex + 1 }}</template><!-- 图片预览 --><template#image="{ row }"><div><divclass="tdesign-demo-image-viewer__base"><t-image-viewer:images="[row.image]"><template#trigger="{ open }"><divclass="tdesign-demo-image-viewer__ui-image"><imgalt="test":src="row.image"class="tdesign-demo-image-viewer__ui-image--img"/><divclass="tdesign-demo-image-viewer__ui-image--hover"@click="open"><span><BrowseIconsize="1.4em"/> 预览 </span></div></div></template></t-image-viewer></div></div></template><!-- 价格拼接 --><template#price="{ row }"> {{ isDecimals(row.price) ? row.price : row.price + '.00' }} </template><!-- 按钮处理 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl">删除</a><aclass="font-bt">编辑</a><aclass="delete">禁用</a></div></template></t-table><!-- 分页 --><t-paginationv-if="total > 10"v-model="pagination.pageNum"v-model:pageSize="pagination.pageSize":total="total"@change="onPageChange"/></div></div></div></template><scriptsetuplang="jsx">import{ ref,onMounted }from'vue';import{COLUMNS}from'./constants'import{ getProjectList }from'@/api/serve'const data =ref([]);const total =ref(0);const dataLoading =ref(false)// 加载中const pagination =ref({pageSize:10,pageNum:1})//生命周期onMounted(()=>{getList()})//调用接口constgetList=async()=>{const res =awaitgetProjectList(pagination.value) data.value = res.data.records total.value =Number(res.data.total)}// 翻页设置当前页constonPageChange=(val)=>{ pagination.value.pageNum = val.current pagination.value.pageSize = val.pageSize getList()}//判断当前参数是否包含小数点constisDecimals=(val)=>{if(String(val).indexOf('.')>-1){returntrue;}returnfalse;}</script>

3.1.4 抽取组件

我们完成了列表查询以后,发现index.vue中已经有了不少的代码了,后面我还有搜索表单、新增、编辑、删除、禁用等功能,如果所有的内容都放在同一个vue中不太好,原因有两个,第一不太好阅读,后期修改调试不方便;第二不通用,假如其他页面有相同的功能,不能复用

所以通常情况下,我们都会对一个组件进行封装,封装为一个单独的vue,然后让index.vue去引用

在这里插入图片描述

抽取组件

我们在pages/serve/plan/project目录中新增一个目录components,新增一个TableList.vue组件

我们可以把index.vue中的代码全粘贴过来进行改造,其中调用接口、接口的参数、具体的方法还是在父组件中执行

  • 如果子组件需要让父组件传递属性,需要在子组件中定义defineProps并需要指明类型
  • 如果子组件需要调用父组件的方法,需要在子组件中定义defineEmits需要指定方法列表
  • 如果子组件需要监听父组件的参数变化,则需要使用watch来监听

代码如下
文件路径:pages/serve/plan/project/components/TableList.vue

<template><divclass="baseList"><divclass="tableBoxs"><!-- 当数据为空需要占位时,会显示 cellEmptyContent --><t-table:data="data":columns="COLUMNS":row-key="rowKey"vertical-align="middle":hover="true":loading="dataLoading"table-layout="fixed"table-content-width="100%"><!-- 处理序号 --><template#rowIndex="{ rowIndex }"> {{ rowIndex + 1 }} </template><!-- 图片预览及展示 --><template#image="{ row }"><divclass="tdesign-demo-image-viewer__base"><t-image-viewer:images="[row.image]"><template#trigger="{ open }"><divclass="tdesign-demo-image-viewer__ui-image"><imgalt="test":src="row.image"class="tdesign-demo-image-viewer__ui-image--img"/><divclass="tdesign-demo-image-viewer__ui-image--hover"@click="open"><span><BrowseIconsize="1.4em"/> 预览 </span></div></div></template></t-image-viewer></div></template><!-- 价格拼接 --><template#price="{ row }"> {{ isDecimals(row.price) ? row.price : row.price + '.00' }} </template><!-- 操作栏 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl">删除</a><aclass="font-bt">编辑</a><aclass="delete">禁用</a></div></template></t-table><t-paginationv-if="total > 10":total="total"v-model:current="pagination.pageNum"v-model:pageSize="pagination.pageSize"@change="onPageChange"/></div></div></template><scriptsetuplang="ts">import{COLUMNS}from'../constants'// 行的keyconst rowKey ='index'const props =defineProps({data:{type: Object,default:()=>{return{}}},// 总条数total:{type: Number,default:0},pagination:{type: Object,default:()=>{return{}}},// 加载状态dataLoading:{type: Boolean,default:false}})//声明方法const emit =defineEmits(['onPageChange'])//点击翻页constonPageChange=(val)=>{emit('onPageChange', val)}constisDecimals=(val)=>{if(String(val).indexOf('.')>-1){returntrue}returnfalse}</script>

修改index.vue,删除之前的<t-table>标签的内容,与当前标签相关的配置也可以删除(TabalList中已定义)
在index.vue中引入新创建的组件

import TableList from './components/TableList.vue' 

<template></template>标签定义TableList组件

<template><divclass="min-h serveProject bg-wt"><TableList:data="data":total="total":pagination="pagination":dataLoading="dataLoading"@onPageChange="onPageChange"></TableList></div></template>

参数和方法的传递
:data、:pagination、:total这三个就是TableList组件需要的变量,通过这三个属性传递
@getCurrent、@isDecimals这两个就是TableList组件需要的方法

index.vue最终代码

<template><divclass="min-h serveProject bg-wt"><TableList:data="data":total="total":pagination="pagination":dataLoading="dataLoading"@onPageChange="onPageChange"></TableList></div></template><scriptsetuplang="jsx">import{ ref,onMounted }from'vue';import{ getProjectList}from'@/api/serve'import TableList from'./components/TableList.vue'const data =ref([]);const total =ref(0);const dataLoading =ref(false)// 加载中const pagination =ref({pageNum:1,pageSize:10})//初始完成后执行查询方法onMounted(()=>{getList();})//翻页设置当前页constonPageChange=(val)=>{ pagination.value.pageNum = val.current pagination.value.pageSize = val.pageSize getList()}//调用接口方法constgetList=async()=>{const res =awaitgetProjectList(pagination.value); data.value = res.data.records; total.value =Number(res.data.total)}</script>
封装组件的过程中,大家一定要一点点调试,经常到网页中查看效果,千万不要一下子把代码写完再调试

3.1.5 封装搜索组件

我们现在已经实现了列表的查询,但是目前还不完善,因为在原型图中,还有搜索框没有添加,就是接口的搜索条件还没有添加

在这里插入图片描述


我们接下来就开发这个功能,我们可以直接去定义搜索组件,然后在index.vue中去引用

3.1.5.1 定义SearchFrom.vue组件

在pages/serve/plan/project/components路径新建SearchFrom.vue

代码如下:

<template><divclass="formBox"><t-formref="form":model="searchData"label-width="98"><t-row><t-col><t-form-itemlabel="护理项目名称:"name="name"><t-inputplaceholder="请输入内容"v-model="searchData.name"class="form-item-content"type="search"clearable@clear="handleClear('name')"/></t-form-item></t-col><t-col><t-form-itemlabel="状态:"name="status"><t-selectclearablev-model="searchData.status"placeholder="请输入内容"@clear="handleClear('status')"><t-optionv-for="(item, index) in statusData":key="index":value="item.id":label="item.value"title=""/></t-select></t-form-item></t-col><t-colclass="searchBtn"><buttontype="button"class="bt-grey wt-60"@click="handleReset()">重置</button><buttontype="button"class="bt wt-60"@click="handleSearch()">搜索</button></t-col></t-row></t-form></div></template><scriptsetuplang="ts">import{ ref }from'vue';import{ statusData }from'@/utils/commonData'const form =ref(null);/* const searchData = ref({ name: '', status: 0 }); *///接收变量defineProps({searchData:{type: Object,default:()=>({})}})//声明方法const emits =defineEmits(['handleReset','handleSearch','handleClear'])//重置搜索框consthandleReset=()=>{emits('handleReset')}//搜索consthandleSearch=()=>{emits('handleSearch')}//清空consthandleClear=(val)=>{emits('handleClear',val)}</script>
TDesign组件参考链接Grid栅格:https://tdesign.tencent.com/vue-next/components/gridForm 表单:https://tdesign.tencent.com/vue-next/components/formInput输入框:https://tdesign.tencent.com/vue-next/components/input

Button 按钮:https://tdesign.tencent.com/vue-next/components/button
注意:其中的class样式,是专业的前端人员调试后的,大家直接使用即可,比如:

在这里插入图片描述
3.1.5.2 在父组件中引用SearchFrom.vue
<template><divclass="min-h serveProject bg-wt"><!-- 搜索框 --><SearchFrom:searchData="pagination"@handleClear="handleClear"@handleReset="handleReset"@handleSearch="handleSearch"></SearchFrom><TableList:data="data":total="total":pagination="pagination":dataLoading="dataLoading"@onPageChange="onPageChange"></TableList></div></template><scriptsetuplang="jsx">import{ ref, onMounted }from'vue';import{ getProjectList }from'@/api/serve'import TableList from'./components/TableList.vue'import SearchFrom from'./components/SearchFrom.vue'//其他代码省略.....//重置搜索框consthandleReset=()=>{//重置页码 pagination.value ={pageNum:1,pageSize:10}getList()}//搜索consthandleSearch=()=>{//点击了搜索按钮之后,就需要重置页码 pagination.value.pageNum =1getList()}//清空consthandleClear=(val)=>{if(val ==='name'){delete pagination.value.name }else{delete pagination.value.status } pagination.value ={//把pagination对象的所有字段,都填充进来...pagination.value }getList()}</script>

3.2 护理项目-新增

3.2.1 需求回顾

我们先查看原型,在列表的右上角有一个按钮,点击之后会出现弹窗,可以输入表单数据

在这里插入图片描述


弹窗(新增或编辑)

在这里插入图片描述

依据原型,弹窗中的内容有很多,分别是

  • 护理项目名称
  • 价格
  • 单位
  • 排序
  • 状态
  • 护理图片
  • 护理项目描述

在去填写这些字段的时候,PRD文档中详细说明了校验规则

在这里插入图片描述

这里面所有的字段,都在一个表单中,然后在表单中有各式各样的输入项,像这些所有内容,我们都可以在TDesign组件中去找到合适的内容进行填充

3.2.2 新增或编辑Dialog(弹窗)

TDesign弹窗组件:https://tdesign.tencent.com/vue-next/components/dialog

在pages/serve/plan/project/components目录新增一个DialogFrom.vue
注意:可以从讲义中直接拷贝到代码中进行开发,我们重点练习vue部分相关的代码

<!-- 护理项目新增编辑弹窗 --><template><divclass="dialog-form"><t-dialogv-model:visible="formVisible":header="title + '护理项目'":footer="false":on-close="onClickCloseBtn"><template#body><!-- 表单内容 --><divclass="dialogCenter"><divclass="dialogOverflow"><t-formref="form":data="formData":rules="rules":label-width="110":reset-type="resetType"@reset="onClickCloseBtn"@submit="onSubmit"><t-form-itemlabel="护理项目名称:"name="name"><t-inputv-model="formData.name"class="wt-400"placeholder="请输入"clearableshow-limit-number:maxlength="10"></t-input></t-form-item><t-form-itemlabel="价格:"name="price"><t-input-numberv-model="formData.price":min="0":step="10"placeholder="0.00":decimal-places="2"@blur="textBlurPrice"@change="textBlurPrice"></t-input-number></t-form-item><t-form-itemlabel="单位:"name="unit"><t-inputv-model="formData.unit"class="wt-400"placeholder="请输入"clearableshow-limit-number:maxlength="5"></t-input></t-form-item><t-form-itemlabel="排序:"name="orderNo"><t-input-numberv-model="formData.orderNo":min="minNumber"@blur="textBlurNo"@change="textBlurNo"></t-input-number></t-form-item><t-form-itemlabel="状态:"name="status"><t-radio-groupv-model="formData.status"><t-radiov-for="(item, index) in statusData":key="index":value="item.id">{{ item.value }}</t-radio></t-radio-group></t-form-item><t-form-itemlabel="护理图片:"name="image"><t-uploadref="uploadRef"v-model="photoFile"action="api/common/upload":autoUpload="autoUpload"theme="image":size-limit="sizeLimit"tips="图片大小不超过2M,仅支持上传PNG JPG JPEG类型图片"accept="image/*":before-upload="beforeUpload"@remove="remove"@fail="handleFail"@success="handleSuccess"></t-upload></t-form-item><t-form-itemlabel="护理项目描述:"name="nursingRequirement"><t-textareav-model="formData.nursingRequirement"class="wt-400"placeholder="请输入"show-limit-number:maxlength="50"></t-textarea></t-form-item><t-form-itemclass="dialog-footer"><div><buttonclass="bt bt-grey wt-60"type="reset">取消</button><buttontheme="primary"type="submit"class="bt wt-60"><span>确定</span></button></div></t-form-item></t-form></div></div></template></t-dialog></div></template><scriptsetuplang="ts">import{ ref, watch }from'vue'import{ MessagePlugin, ValidateResultContext }from'tdesign-vue-next'// 基础数据import{ statusData }from'@/utils/commonData'// 获取父组件值、方法const props =defineProps({// 弹层隐藏显示visible:{type: Boolean,default:false},// 详情数据data:{type: Object,default:()=>{return{}}},// 最小值minNumber:{type: Number,default:1},// 标题title:{type: String,default:'新增'},})// ------定义变量------// 触发父级事件constemit: Function =defineEmits(['handleClose','fetchData','handleAdd','handleEdit'])const resetType =ref('empty')// 重置表单const form =ref()// 表单const formVisible =ref(false)// 弹窗// 表单数据const formData = ref<Object | any>({status:1,orderNo:1})const autoUpload =ref(true)// 是否在选择文件后自动发起请求上传文件const photoFile =ref([])// 绑定上传的文件const sizeLimit =ref({size:2,unit:'MB',message:'图片大小超过2m,请重新上传'})// 图片的大小限制// 表单校验const rules ={name:[// 名称校验{required:true,message:'护理项目名称为空,请输入护理项目名称',type:'error',trigger:'blur'}],// 费用校验price:[{required:true,message:'价格为空,请输入价格',type:'error',trigger:'blur'},{validator:(val)=> val >=0.01,message:'价格为空,请输入价格',type:'error',trigger:'change'}],// 单位unit:[{required:true,message:'单位为空,请输入单位',type:'error',trigger:'blur'}],// 排序orderNo:[{required:true,message:'排序为空,请输入排序',type:'error',trigger:'blur'},{validator:(val)=> val >=1,message:'排序为空,请输入排序',type:'error',trigger:'change'}],// 状态status:[{required:true,message:'状态为空,请选择状态',type:'error',trigger:'change'}],// 护理图片image:[{required:true,message:'护理图片为空,请上传护理图片',type:'error',trigger:'change'}],// 项目描述nursingRequirement:[{required:true,message:'护理项目描述为空,请输入护理项目描述',type:'error',trigger:'blur'}]}// 弹窗标题const title =ref()// 监听器,监听父级传递的visible值,控制弹窗显示隐藏watch(()=> props.visible,()=>{ formVisible.value = props.visible title.value = props.title })// 监听器,监听父级传递的data值,控制表单数据watch(()=> props.data,(val)=>{ formData.value = val const obj ={url: val.image } photoFile.value.push(obj)})// -----定义方法------// 提交表单constonSubmit=(result: ValidateResultContext<FormData>)=>{if(result.validateResult ===true){if(props.title ==='新增'){// 调用新增接口emit('handleAdd', formData.value)}else{// 调用编辑接口emit('handleEdit', formData.value)}}}// 清除表单数据consthandleClear=()=>{// 重置表单 form.value.reset() formData.value.orderNo =1 formData.value.status =1 photoFile.value =[]}// 点击取消关闭constonClickCloseBtn=()=>{handleClear()emit('handleClose')}// // 监听价格consttextBlurPrice=()=>{const data =Number(formData.value.price)minPrice(data)}// 监听排序consttextBlurNo=()=>{const data =Number(formData.value.orderNo)minNum(data)}// 当前输入的金额小于0的时候显示0.00constminPrice=(val)=>{if(val <0){ formData.value.fee ='0.00'}}// 当前输入的排序小于等于1的时候显示1constminNum=(val)=>{if(val <=1){ formData.value.orderNo =1}}// 移除图片时将图片设置为默认图片constremove=()=>{ photoFile.value =[] formData.value.image =''}// 上传图片失败consthandleFail=({ file })=>{ MessagePlugin.error(`图片 ${file.name} 上传失败`)}// 上传成功后触发。consthandleSuccess=(params)=>{const photo = params.response.data formData.value.image = photo photoFile.value[0].response.url = photo photoFile.value[0].url = photo }// 限制图片的大小constbeforeUpload=(file)=>{if(file.size >2*1024*1024){ MessagePlugin.error('图片大小超过2M,请重新上传')returnfalse}returntrue}// 向父组件暴露数据与方法defineExpose({ handleClear })</script>

其中visible属性控制是否弹出弹窗,true:弹出,false:退出

3.2.3 TableList组件中添加”新增按钮”

调出弹窗的按钮在列表的左上角:新增护理项目按钮

在这里插入图片描述

在TableList组件中新增按钮代码,代码如下:

<divclass="newBox"><buttonclass="bt wt-120"@click="handleBulid()"> 新增护理项目 </button></div> 在js代码中增加方法handleBulid,来打开弹窗 <scriptsetuplang="ts">//声明方法const emit =defineEmits(['onPageChange','handleBulid'])//新增按钮consthandleBulid=()=>{emit('handleBulid')}</script>

在index.vue中去引用,我们在父组件中中去控制visible属性,需要给刚才定义的按钮绑定(新增护理项目)

<template> <TableList :data="data" :total="total" :pagination="pagination" @getCurrent="getCurrent" @isDecimals="isDecimals" @handleBulid="handleBulid" //注意这里需要在TableList组件中调用父组件的方法 ></TableList><DialogFrom:visible="visible"@handleClose="handleClose"></DialogFrom></template><scriptsetuplang="ts">import DialogFrom from'./components/DialogFrom.vue'//是否显示弹窗var visible =ref(false)//点击新增护理项目 按钮 把visible设置为true,弹出consthandleBulid=()=>{ visible.value =true;}//点击弹窗中的关闭或取消,关闭弹窗consthandleClose=()=>{ visible.value =false;}</script>

3.3.4 编写新增接口

在src/api/serve.ts文件,定义新增接口,代码如下:

// 护理项目添加exportfunctionprojectAdd(params){return request.post<ProjecListModel>({url:'/nursing_project',data: params })}

3.3.5 index组件与Dialog组件整合

index.vue中继续完善DialogFrom组件的内容,代码如下:

<template><DialogFromref="formRef":visible="visible":title="title"@handleClose="handleClose"@handle-add="handleAdd"></DialogFrom></template><scriptsetuplang="ts">import{ onMounted, ref }from'vue'import TableList from'./components/TableList.vue'import SearchFrom from'./components/SearchFrom.vue'import DialogFrom from'./components/DialogFrom.vue'import{ getProjectList,projectAdd }from'@/api/serve'import{ MessagePlugin }from'tdesign-vue-next'var visible =ref(false)const formRef =ref(null)const title =ref('')// 弹窗标题const formBaseData =ref({})// 弹窗表单内容consthandleBulid=()=>{ title.value ='新增' visible.value =true;}consthandleClose=()=>{ visible.value =false;}// 添加consthandleAdd=async(val)=>{const res =awaitprojectAdd(val)if(res.code ===200){ MessagePlugin.success('添加成功')getList()handleClose() formRef.value.handleClear()}else{ MessagePlugin.error(res.msg)}}</script>
  • formRef就是指子组件,定义为了一个对象
  • formRef.value.handleClear() 就是父组件中直接执行子组件暴露的方法

子组件向父组件暴露数据和方法,可以让父组件去执行

// 向父组件暴露数据与方法defineExpose({ handleClear })

3.3 护理项目-编辑

3.3.1 需求梳理

当我点击列表中的编辑按钮,首先需要回显数据,然后修改完成以后,点击确定是修改数据

在这里插入图片描述

也就是说,这里面我们需要准备两个接口,分别是:根据id查询详情修改

3.3.2 列表中编辑打开弹窗

找到TableList组件中的操作栏,修改编辑a标签

在这里插入图片描述
<!-- 操作栏 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl">删除</a><aclass="font-bt"@click="handleEdit(row)">编辑</a><aclass="delete">禁用</a></div></template>

在当前组件中,需要调用父组件的方法

在这里插入图片描述
//引入该组件,需要传递方法const emit =defineEmits(['getCurrent','handleBulid','handleEdit'])//编辑 参数为一行数据consthandleEdit=(row)=>{emit('handleEdit', row)}

在index.vue组件中添加对应的方法,需要调用接口查询护理项目的详情

<template><TableList:data="data":total="total":pagination="pagination":dataLoading="dataLoading"@onPageChange="onPageChange"@handleBulid="handleBulid"@handleEdit="handleEdit"></TableList><!-- 新增或编辑弹窗 --><DialogFromref="formRef":title="title":visible="visible":data="formBaseData"@handleClose="handleClose"@handleAdd="handleAdd"></DialogFrom></template><scriptsetuplang="ts">//添加接口import{ getProjectList,projectAdd,getProjectDetails }from'@/api/serve'const title =ref('')// 弹窗标题const formBaseData =ref({})// 弹窗表单内容//编辑consthandleEdit=(val)=>{// 将弹窗的标题 title.value ='编辑'// 获取详情getDetails(val.id)// 显示弹窗 visible.value =true}// 获取详情数据constgetDetails=async(id)=>{const res =awaitgetProjectDetails(id)// 获取列表数据if(res.code ===200){ formBaseData.value = res.data }}</script>

在src/api/serve.ts文件,定义查询详情接口,参考Knife4j在线接口文档

// 获取护理项目详情exportfunctiongetProjectDetails(id){return request.get<ProjecListModel>({url:`/nursing_project/${id}`})}

修改DialogFrom组件,回显数据,这个能够回显的原因有两个

第一:在DialogFrom组件中定义了接收了父组件的data数据

第二:在DialogFrom组件中定义了侦听,一旦数据发生变化就会重新给表单数据赋值

<scriptsetuplang="ts">import{ ref, watch }from'vue'// 基础数据import{ statusData }from'@/utils/commonData'import{ MessagePlugin, ValidateResultContext }from'tdesign-vue-next'import{ rules }from'./rules'const props =defineProps({// 监听器,监听父级传递的data值,控制表单数据watch(()=> props.data,(val)=>{ formData.value = val const obj ={url: val.image } photoFile.value.push(obj)})</script>

我们现在打开编辑弹窗,数据就可以回显了,效果如下:

在这里插入图片描述

3.3.3 修改数据

我们刚才是回显了护理项目的详细数据,现在当我们修改了数据之后,点击确定就需要调用后端的修改接口了,由于我们之前写过新增,它们的思路基本是一致的,并且新增和编辑复用了弹窗,我们现在只需要编写修改的接口即可。

在src/api/serve.ts文件,定义修改护理项目的接口,参考Knife4j在线接口文档

// 护理项目编辑exportfunctionprojectUpdate(params: ProjecListModel){return request.put<ProjecListModel>({ url:`/nursing_project`, data: params })}

由于我们之前在DialogFrom表单中已经定义了修改的方法,并且让它去调用了父组件的方法,我们现在只需要在父组件中去定义修改方法即可

<template><SearchFrom:searchData="pagination"@handleReset="handleReset"@handleSearch="handleSearch"></SearchFrom><TableList:data="data":total="total":pagination="pagination"@getCurrent="getCurrent"@isDecimals="isDecimals"@handleBulid="handleBulid"@handleEdit="handleEdit"></TableList><DialogFromref="formRef":visible="visible":data="formBaseData":title="title"@handleClose="handleClose"@handle-add="handleAdd"@handleEdit="handleEditForm"></DialogFrom></template><scriptsetuplang="ts">import{ getProjectList,projectAdd,getProjectDetails,projectUpdate }from'@/api/serve'// 修改数据consthandleEditForm=async(val)=>{const res =awaitprojectUpdate(val)if(res.code ===200){ MessagePlugin.success('编辑成功')getList()handleClose() formRef.value.handleClear()}else{ MessagePlugin.error(res.msg)}}//编辑consthandleEdit=(val)=>{// 将弹窗的标题 title.value ='编辑'// 获取详情getDetails(val.id)// 显示弹窗 visible.value =true}</script>
大家注意:
在index.vue中有两个Edit方法,它们的作用是不同的handleEdit 被列表中的编辑按钮触发,作用是打开弹窗,获取详情,在TableList被引用handleEditForm 弹窗中的确定按钮触发,作用是修改数据 ,在DialogFrom中被引用

3.4 护理项目-删除

当我们点击了删除按钮,就会先弹出一个确认框,再来决定是否需要删除

在这里插入图片描述


由于像这样的删除弹窗,在当前项目中的很普遍,应用的地方很多,所以,像这样的弹窗都会封装为一个公共的组件来让项目使用

我们项目中公共组件位置在src/components目录中

其中的删除组件在src/components/OperateDialog/index.vue

我们先来分析一下这个代码:

<!--操作弹层--><template><divclass="deleteDialog baseDialog"><t-dialogv-model:visible="dialogVisible":header="title ? title : '确认删除'":footer="false":on-close="handleClose":on-confirm="handleSubmit"><divv-if="title === '确认驳回'"> 驳回申请后,该流程将自动驳回至发起人,是否继续? </div><divv-else-if="title === '确认提交'"> 账单审批通过后,应退金额不可再次修改。完成退款操作后,退款金额将退到预缴款余额中,最终随退住办理完结时一起退还给老人,是否确定提交账单? </div><divv-else><divv-if="text">此操作将{{ text }},是否继续?</div><divv-else>此操作将删除该{{ deleteText }},是否继续?</div></div><!-- 此操作将永久删除这条信息,是否继续? --><divclass="dialog-footer"><buttontheme="primary"type="submit"class="bt-grey wt-60"@click="handleClose"><span>取消</span></button><buttontheme="primary"type="submit"class="bt wt-60"@click="handleSubmit"><span>确定</span></button></div></t-dialog></div></template><scriptsetuplang="ts">import{ ref, watch }from'vue'// 获取父组件值、方法const props =defineProps({// 弹层隐藏显示visible:{type: Boolean,default:false},title:{type: String,default:''},text:{type: String,default:''},deleteText:{type: String,default:''}})// ------定义变量------const emit =defineEmits(['handleClose','handleDelete'])// 子组件获取父组件事件传值const dialogVisible =ref(false)watch(()=> props.visible,(newVal)=>{ dialogVisible.value = newVal })// ------定义方法------// 关闭弹层consthandleClose=()=>{emit('handleClose')}// 提交确定删除consthandleSubmit=()=>{emit('handleDelete')}</script>

我们想要使用这个组件,想要传入几个值

:visible 控制弹窗的调出:deleteText 删除的具体项目业务提示 ,比如:此操作将删除该护理项目,是否继续?handleClose 关闭弹窗的方法handleDelete 调用后端删除接口,执行数据删除

下面我们就进入代码开发,首先我们要做的是调出该弹窗,然后才会执行删除

调出弹窗是在列表页中的”删除”按钮

在这里插入图片描述

我们需要修改TableList组件,来调出弹窗,代码如下:

<template><divclass="newBox"><buttonclass="bt wt-120"@click="handleBulid()">新增护理项目</button></div><!-- 当数据为空需要占位时,会显示 cellEmptyContent --><t-tablerowKey="index":data="data":columns="COLUMNS":stripe="stripe":bordered="bordered":hover="hover":table-layout="tableLayout ? 'auto' : 'fixed'":size="size":pagination="pagination.total > 10 ? pagination : null":show-header="showHeader"cell-empty-content="-"resizable@page-change="getCurrent"><!-- 操作栏 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl"@click="handleClickDelete(row)">删除</a><aclass="font-bt"@click="handleEdit(row)">编辑</a><aclass="delete">禁用</a></div></template></t-table></template><scriptsetuplang="ts">//引入该组件,需要传递方法const emit =defineEmits(['getCurrent','isDecimals','handleBulid','handleEdit','handleClickDelete'])// 点击删除consthandleClickDelete=(row)=>{emit('handleClickDelete', row)}</script>

准备删除接口

在src/api/serve.ts文件,定义删除护理项目的接口,参考Knife4j在线接口文档

// 护理项目删除exportfunctionprojectDelete(id){return request.delete({url:`/nursing_project/${id}`})}

然后在index.vue中添加handleClickDelete方法来控制弹窗的出现,不过,我们也需要在index.vue中引入delete弹窗,代码如下:

<template><TableList:data="data":total="total":pagination="pagination"@getCurrent="getCurrent"@isDecimals="isDecimals"@handleBulid="handleBulid"@handleEdit="handleEdit"@handleClickDelete="handleClickDelete"></TableList><!-- 删除弹层 --><Delete:visible="dialogDeleteVisible":delete-text="operateText"@handle-delete="handleDelete"@handle-close="handleDeleteClose"></Delete></template><scriptsetuplang="ts">// 删除弹层import Delete from'@/components/OperateDialog/index.vue'import{ getProjectList,projectAdd,getProjectDetails,projectUpdate,projectDelete }from'@/api/serve'const dialogDeleteVisible =ref(false)// 控制删除弹层显示隐藏const operateText =ref('护理项目')// 要操作的内容提示const typeId =ref('')// 设置删除id// 确认删除consthandleDelete=async()=>{const res=awaitprojectDelete(typeId.value)if(res.code ===200){ dialogDeleteVisible.value =false MessagePlugin.success('删除成功')getList()}}// 点击删除consthandleClickDelete=(val)=>{ typeId.value = val.id dialogDeleteVisible.value =true}// 关闭删除弹层consthandleDeleteClose=()=>{ dialogDeleteVisible.value =false}</script>
  • 在TableList组件传递handleClickDelete,并编写handleClickDelete方法逻辑
  • 引入Delete组件
    • 方法:handle-close 关闭删除弹窗
    • 方法:handle-delete 调用接口删除
    • 属性:visible 控制删除弹层显示隐藏
    • 属性:delete-text 要操作的内容提示

3.5 护理项目-启用禁用

跟删除弹窗类似,在点击禁用的时候,也会出现弹窗,效果如下

在这里插入图片描述
需求回顾:只有禁用才会有弹窗确认提示,如果是启用,则不会弹窗,直接启用

因为这个功能也是通用的,在项目也已经提供了公共的组件

禁用组件路径:src/components/Forbidden/index.vue

<!--删除弹层--><template><divclass="deleteDialog baseDialog"><t-dialogv-model:visible="dialogVisible"header="确认禁用":footer="false":on-close="handleClose":on-confirm="handleSubmit"> 此操作将禁用该{{ text }},是否继续? <divclass="dialog-footer"><buttontheme="primary"type="submit"class="bt-grey wt-60"@click="handleClose"><span>取消</span></button><buttontheme="primary"type="submit"class="bt wt-60"@click="handleSubmit"><span>确定</span></button></div></t-dialog></div></template><scriptsetuplang="ts">import{ ref, watch }from'vue'// 获取父组件值、方法const props =defineProps({// 弹层隐藏显示visible:{type: Boolean,default:false},text:{type: String,default:''}})// ------定义变量------const emit =defineEmits(['handleClose','handleSubmit'])// 子组件获取父组件事件传值const dialogVisible =ref(false)watch(()=> props.visible,(newVal, oldVal)=>{ dialogVisible.value = newVal })// ------定义方法------// 关闭弹层consthandleClose=()=>{emit('handleClose')}// 提交确定删除consthandleSubmit=()=>{emit('handleSubmit')}</script>

我们想要使用这个组件,想要传入几个值

:visible 控制弹窗的调出:text 禁用的具体项目业务提示 ,比如:此操作将禁用该护理项目,是否继续?handleClose 关闭弹窗的方法handleSubmit 调用后端禁用接口,执行禁用

下面我们就进入代码开发,首先我们要做的是调出该弹窗,然后才会执行禁用

调出弹窗是在列表页中的”禁用”按钮

在这里插入图片描述

我们需要修改TableList组件,来调出弹窗,并且我们也发现了,如果是启用是绿色按钮,如果是禁用是红色按钮,这个需要使用状态的不同来控制按钮的颜色,代码如下:

<template><divclass="newBox"><buttonclass="bt wt-120"@click="handleBulid()">新增护理项目</button></div><!-- 当数据为空需要占位时,会显示 cellEmptyContent --><t-tablerowKey="index":data="data":columns="COLUMNS":stripe="stripe":bordered="bordered":hover="hover":table-layout="tableLayout ? 'auto' : 'fixed'":size="size":pagination="pagination.total > 10 ? pagination : null":show-header="showHeader"cell-empty-content="-"resizable@page-change="getCurrent"><!-- 操作栏 --><template#op="{ row }"><divclass="operateCon"><aclass="btn-dl"@click="handleClickDelete(row)">删除</a><aclass="font-bt"@click="handleEdit(row)">编辑</a><aclass="delete":class="row.status === 1 ? 'btn-dl' : 'font-bt'"@click="handleForbidden(row)">{{ row.status === 1 ? '禁用' : '启用' }}</a></div></template></t-table></template><scriptsetuplang="ts">import{ watch, ref }from'vue'import{COLUMNS}from'../constants'//引入该组件,需要传递方法const emit =defineEmits(['getCurrent','isDecimals','handleBulid','handleEdit','handleClickDelete','handleForbidden'])// 禁用consthandleForbidden=(row)=>{emit('handleForbidden', row)}</script>

准备删除接口

在src/api/serve.ts文件,定义禁用护理项目的接口,参考Knife4j在线接口文档

// 护理项目禁用启用exportfunctionprojectStatus(params){return request.put({url:`/nursing_project/${params.id}/status/${params.status}`})}

然后在index.vue中添加handleForbidden方法来控制弹窗的出现,不过,我们也需要在index.vue中引入禁用弹窗,代码如下:

<template><TableList:data="data":total="total":pagination="pagination"@getCurrent="getCurrent"@isDecimals="isDecimals"@handleBulid="handleBulid"@handleEdit="handleEdit"@handleClickDelete="handleClickDelete"@handleForbidden="handleForbidden"></TableList><!-- 禁用弹层 --><Forbidden:visible="dialogVisible":text="operateText"@handle-submit="handleForbiddenSub"@handle-close="handleForbiddenClose"></Forbidden></template><scriptsetuplang="ts">import{ onMounted, ref }from'vue'import TableList from'./components/TableList.vue'import SearchFrom from'./components/SearchFrom.vue'import DialogFrom from'./components/DialogFrom.vue'// 删除弹层import Delete from'@/components/OperateDialog/index.vue'// 禁用弹窗import Forbidden from'@/components/Forbidden/index.vue'import{ getProjectList,projectAdd,getProjectDetails,projectUpdate,projectDelete,projectStatus }from'@/api/serve'import{ MessagePlugin }from'tdesign-vue-next'var data =ref([])var total =ref(0)var visible =ref(false)const formRef =ref(null)const title =ref('')// 弹窗标题const formBaseData =ref({})// 弹窗表单内容const dialogDeleteVisible =ref(false)// 控制删除弹层显示隐藏const operateText =ref('护理项目')// 要操作的内容提示const typeId =ref('')// 设置删除idconst dialogVisible =ref(false);const typeStatus =ref(null)// 禁用启用const statusText =ref('')// 启用禁用提示//确定禁用consthandleForbiddenSub=async()=>{const params ={id: typeId.value,status: typeStatus.value }const res =awaitprojectStatus(params)if(res.code ===200){ dialogVisible.value =false MessagePlugin.success(statusText.value)getList()}}// 禁用弹窗consthandleForbidden=(val)=>{ typeId.value = val.id if(val.status ===1){ dialogVisible.value =true typeStatus.value =0 statusText.value ='禁用成功'}else{ typeStatus.value =1handleForbiddenSub() statusText.value ='启用成功'}}// 关闭禁用弹窗consthandleForbiddenClose=()=>{ dialogVisible.value =false}</script>
  • 在TableList组件传递handleForbidden,并编写handleForbidden方法逻辑
  • 引入Forbidden组件
    • 方法:handleForbiddenClose 关闭禁用弹窗
    • 方法:handleForbiddenSub 调用接口启用或禁用
    • 属性:dialogVisible 控制禁用弹层显示隐藏
    • 属性:operateText 要操作的内容提示
    • 属性:typeId 护理项目id临时存储
    • 属性:typeStatus 护理项目状态临时存储(禁用 | 启用)

4 前后端联调

4.1 什么是前后端联调

在这里插入图片描述

现在的开发基本都是前后端分离的项目,对于前端来说,后端还没有提供接口的时候,前端需要的数据从哪里来呢?通常情况下,可以自己造数据写死,还可以使用mock来造数据,别管是哪种方式,都没有调用真正的后台接口。假如现在后端已经给提供了接口,前端所需要的数据就要替换为后端提供的接口数据了。这个时候就需要进行前后端的联调,这个过程就是前后端接口联调

4.2 为什么要联调

为什么要进行联调呢?我们之前不是已经分析了需求,也定义好了接口了吗?

可能的原因如下几条:

  • 后端没有做严谨的测试,依赖前端调用接口达到测试的目的
  • 前端没有做严谨的测试,想要调用后台接口输出数据达到测试的目的
  • 前后端整合一起才是完整的靠谱的项目
  • 多方配合尽早找出问题,交付客户稳定的产品

4.3 联调的最佳实践

  • 定义明确的接口规范
    • 响应码
    • 入参的结构(数据类型、传参的方式)
    • 出参的数据结构
  • 本地联调测试
    • 前后端都应该在本地进行自测
  • 制定联调计划
    • 在评估工期的过程中,都需要制定联调计划,要确保项目的开发进度
    • 如果临时制定联调计划,可能会出现前后端人员时间不匹配的情况,耽误工期
  • 保证环境稳定
    • 如果前后端开发都在一起办公,可以先通过局域网链接的方式进行小范围联调
    • 如果不在一起办公,则需要都部署到测试环境(公共的环境)中去进行联调测试
  • 问题记录及修复
    • 在联调过程中,出现问题很正常,比如说接口不兼容、性能瓶颈等。为了避免重复出现问题,开发人员需要记录问题,并及时解决。

Read more

前端权限管理实现:别让用户看到不该看的东西!

前端权限管理实现:别让用户看到不该看的东西! 毒舌时刻 权限管理?听起来就像是前端工程师为了显得自己很专业而特意搞的一套复杂流程。你以为随便加个if语句就能实现权限管理?别做梦了!到时候你会发现,权限逻辑分散在各个组件中,难以维护。 你以为前端权限管理就是最终的安全保障?别天真了!前端权限管理只是为了提高用户体验,真正的安全保障在后端。还有那些所谓的权限管理库,看起来高大上,用起来却各种问题。 为什么你需要这个 1. 用户体验:良好的权限管理可以为不同角色的用户提供不同的界面,提高用户体验。 2. 安全性:前端权限管理可以防止用户访问不该访问的功能,提高应用的安全性。 3. 代码组织:集中的权限管理可以使代码结构更清晰,便于维护。 4. 可扩展性:良好的权限管理设计可以方便地添加新的角色和权限。 5. 合规性:某些行业和地区要求应用必须实现严格的权限控制。 反面教材 // 1. 分散的权限逻辑 function AdminPanel() { const user = useUser(); if (user.role !== 'admin'

Web 毕设篇-适合练手的 Spring Boot Web 毕业设计项目:智驿AI系统(前后端源码 + 数据库 sql 脚本)

Web 毕设篇-适合练手的 Spring Boot Web 毕业设计项目:智驿AI系统(前后端源码 + 数据库 sql 脚本)

🔥博客主页: 【小扳_-ZEEKLOG博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         AI系统具有许多优势         1.0 项目介绍         1.1 项目功能         1.2 用户端功能         2.0 用户登录         3.0 首页界面         4.0 物件管理功能         5.0 用户管理功能         6.0 区域管理功能         7.0 物件日志管理功能         8.0 操作日志         AI系统具有许多优势         1)自动化:AI 系统能够自动化执行任务,减少人力和时间成本。它们可以自动处理大量数据并执行复杂的计算,从而提高效率。         2)智能决策:AI 系统可以通过学习和分析数据来做出智能决策。

前端组件库:别再重复造轮子了

前端组件库:别再重复造轮子了 毒舌时刻 这组件写得跟拼凑似的,一点都不统一。 各位前端同行,咱们今天聊聊前端组件库。别告诉我你还在手动编写所有组件,那感觉就像在没有工具的情况下盖房子——能盖,但效率低得可怜。 为什么你需要组件库 最近看到一个项目,每个组件都要手动编写,样式不统一,维护困难。我就想问:你是在做组件还是在做重复劳动? 反面教材 // 反面教材:手动编写组件 // Button.jsx import React from 'react'; function Button({ children, onClick }) { return ( <button onClick={onClick} style={{ padding: '10px 20px', backgroundColor: '#007bff', color: '