在前面随笔《
基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理
》中我们已经介绍过,对于相关的业务表的界面代码,我们已经尽可能把不同的业务逻辑封装在不同的页面组件中,隔离变化的差异,因此界面组件化后,就可以利用代码生成工具进行统一的界面代码的生成了,而且由于变化的隔离处理,我们实际上维护的代码变得更加方便维护了。本篇随笔介绍在整合代码生成工具进行前端界面的生成的一些思路和实际的界面代码的生成。

1、页面的模块化处理

在前面随笔《
基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理
》中我们已经介绍过,常规页面包含有列表界面,新增、编辑、查看、导入等界面,除了列表页面,其他内容以弹出层对话框的方式进行处理,如下界面示意图所示。

根据以上的页面划分,我们把一个页面分为search.vue、edit.vue、import.vue、view.vue、index.vue,其中index.vue为整合各个组件的主页面,在视图中如下所示。我们每个业务模块都是如此统一划分,因此比较统一,同时也是为后续的代码生成工具批量生成做好准备。

因此在index.vue页面中,我们整合了几个组件页面即可,如下所示。

<template>
  <divclass="main">
    <!--条件及列表展示-->
    <Searchref="searchRef"@show-import="showImport"@show-add="showAdd"@show-view="showView"@show-edit="showEdit" />

    <!--查看详细组件界面-->
    <view-dataref="viewRef" />
    <!--新增、编辑组件界面-->
    <edit-dataref="editRef"@submit="refreshData" />
    <!--模板导入信息-->
    <import-dataref="importRef"@finish="finishImport" />
  </div>
</template>

<scriptsetup lang="ts">import { reactive, ref, onMounted } from'vue';

import Search from
'./search.vue';
import ViewData from
'./view.vue';
import EditData from
'./edit.vue';
import ImportData from
'./import.vue';

1)查看视图页面

我们先以view.vue查看页面为例进行介绍,它是一个查看明细的界面,因此也是一个弹出对话框页面,我们把它的代码处理如下所示。

<template>
  <el-dialogv-if="isVisible"v-model="isVisible"title="查看信息"append-to-body @close="closeDialog">
    <el-formref="viewRef":model="viewForm"label-width="100px">
      <el-tabstype="border-card">
        <el-tab-panelabel="基本信息">
          <el-descriptionstitle="":column="2"border>
            <el-descriptions-itemlabel="显示名称">{{ viewForm.name }}</el-descriptions-item>
            <el-descriptions-itemlabel="Web地址">{{ viewForm.url }}</el-descriptions-item>
            <el-descriptions-itemlabel="Web图标">
              <!--{{ viewForm.webIcon }}-->
              <icon:icon="viewForm.webIcon" />
            </el-descriptions-item>
            <el-descriptions-itemlabel="排序">{{ viewForm.seq }}</el-descriptions-item>
            <el-descriptions-itemlabel="可见">
              <el-tagv-if="viewForm.visible"type="success"effect="dark">可见</el-tag>
              <el-tagv-else type="danger"effect="dark">隐藏</el-tag>
            </el-descriptions-item>
            <el-descriptions-itemlabel="展开">
              <el-tagv-if="viewForm.expand"type="success"effect="dark">展开</el-tag>
              <el-tagv-else type=""effect="dark">收缩</el-tag>
            </el-descriptions-item>
            <el-descriptions-itemlabel="创建时间">
              <el-date-pickerv-model="viewForm.createTime"align="right"type="datetime"placeholder="选择日期"value-format="YYYY-MM-DD HH:mm"disabled/>
            </el-descriptions-item>
            <el-descriptions-itemlabel="特殊标签">{{ viewForm.tag }}</el-descriptions-item>
          </el-descriptions>
        </el-tab-pane>
      </el-tabs>
    </el-form>
    <template#footer>
      <spanclass="dialog-footer">
        <el-button@click="closeDialog">关闭</el-button>
      </span>
    </template>
  </el-dialog>
</template>

其他的js代码采用tyepscript语法,我们把它放在

<scriptsetup lang="ts">
//逻辑代码
</script>

然后我们在js代码中抛出show的方法,便于父组件的调用。

//显示窗口
const show = (id: string | number) =>{if (!$u.test.isNullOrUnDef(id)) {
menu.Get(id).then((data)
=>{
Object.assign(viewForm, data);

isVisible.value
= true; //显示对话框 });
}
};
//暴露组件属性和方法 defineExpose({
show,
});

同时在页面里面,也定义一个表单的对象引用,便于上面模板组件的显示。

const viewRef = ref<FormInstance>(); //表单引用//表单属性定义
let viewForm =reactive({
iD:
'',
pid:
'',
name:
'',
icon:
'',
seq:
'',
functionId:
'',
visible:
0,
expand:
0,
winformType:
'',
url:
'',
webIcon:
'',creator:'',createTime:'',tag:'',
});

如果是查看详细的视图中有树形列表,我们还可以在onMounted的处理中,添加获取树列表的数据即可,如下代码所示。

//挂载的时候初始化数据
onMounted(() =>{//设置默认值
getTree();
});
//初始化树列表 let treedata =ref([]);
const getTree
= () =>{//树列表数据获取 menu.GetAll().then((data) =>{
treedata.value
= []; //树列表清空 var list = data?.items;if(list) {var newTreedata =$u.util.getJsonTree(list, {
id:
'id',
pid:
'pid',
label:
'name',
children:
'children',
});
treedata.value
=newTreedata;
}
});
};

而查看视图的触发,往往在列表的操作按钮中,或者双击表格行进行触发,界面如下所示。

界面如下代码所示。

而调用查看详细页面的事件,传递对应的id并调用组件实例抛出的方法即可。如下代码所示。

//显示查看对话框处理
const viewRef = ref<InstanceType<typeof ViewData>>();functionshowView(id) {
viewRef.value.show(id);
}

至此,一个独立的视图页面组件,以及如何触发调用就完成了,视图页面单独维护,便于代码的管理,同时也隔离了复杂的页面逻辑。

视图页面效果如下所示。

2)新增编辑视图页面

在常规的处理中,往往编辑和新增的界面是差不多的,差异不同的地方,我们可以通过条件 if 的方式进行处理即可。因此可以把两者放在一个组件中实现对话框内容和逻辑处理。

刚才我们提到了Index.vue页面,是对几个组件的统筹处理,如下代码所示。

<template>
  <divclass="main">
    <!--条件及列表展示-->
    <Searchref="searchRef"@show-import="showImport"@show-add="showAdd"@show-view="showView"@show-edit="showEdit" />

    <!--查看详细组件界面-->
    <view-dataref="viewRef" />
    <!--新增、编辑组件界面-->
  <edit-dataref="editRef"@submit="refreshData" />
    <!--模板导入信息-->
    <import-dataref="importRef"@finish="finishImport" />
  </div>
</template>

从上面代码我们看到,在HTML代码中,我们引入对应的组件,并在主查询页面中触发事件即可,如下所示。

其中showAdd和ShowEdit类似,都是调用编辑/新增的对话框,不同的是,通过传递id来辨别是否为新增,如果需要传入pid的父节点信息,那么我们也可以创建一个showAdd的方法。

//新增、编辑表单引用
const editRef = ref<InstanceType<typeof EditData>>();//显示新增对话框
function showAdd(pid?: string |number) {
editRef.value.showAdd(pid);
}
//显示编辑对话框 functionshowEdit(id) {
editRef.value.show(id);
}
//新增/更新后刷新 functionrefreshData() {
searchRef.value.getlist();
}

编辑业务数据的对话框和查看详细的类似,不过这里是需要使用输入控件进行内容编辑修改处理的。

对于一些复杂控件,我们可以自定义组件来简化在界面上的使用,尽可能的快捷、简单。

我们在组件中定义showAdd和show的方法,便于父组件的调用即可,如果传递了id值,我们根据业务对象的get方法获取详细的数据,赋值到表单对象上就可以正常显示了。

//默认标题为[编辑信息],当show传入id为空的时候,为[新建信息]
let title = ref('编辑信息');
let isAdd
= ref(false); //是否新增状态 //显示窗口(编辑/新建) const showAdd = async (pid?: string | number) =>{
title.value
= '新建信息';
isAdd.value
= true;
resetFields();
//清空表单 editForm.pid= pid + '';
isVisible.value
= true; //显示对话框 };
const show
= async (id?: string | number) =>{if (!$u.test.isNullOrUnDef(id)) {
title.value
= '编辑信息';
isAdd.value
= false;
await menu.Get(id).then((data)
=>{ Object.assign(editForm, data);
});
}
else{
title.value
= '新建信息';
isAdd.value
= true;
resetFields();
//清空表单 } isVisible.value = true; //显示对话框 };

抛出这两个实例的方法,供外面调用。

//暴露组件属性和方法
defineExpose({
show,
showAdd,
});

为了承载表单的数据模型,我们创建相关的业务对象

const editRef = ref<FormInstance>(); //表单引用//表单属性定义(初始化)
let editForm =reactive({
id:
'',
pid:
'',
name:
'',
icon:
'',
functionId:
'',
winformType:
'',
url:
'',
seq:
'001',
isTop:
false,
expand:
1,
visible:
1,
webIcon:
'',
tag:
'web', //Web专用 });

保存数据的时候,我们判断表单的rules规则,如果符合通过,那么提交数据并提示用户即可。

//保存数据处理
async functionsubmitData() {var formEl =editRef.value;if (!formEl) return;

await formEl.validate(async (valid)
=>{if(valid) {//验证成功,执行下面方法 var result = false;if(isAdd.value) {
result
= await menu.Create(editForm); //新增保存 } else{
result
= await menu.Update(editForm); //编辑保存 }if(result) {
$u.success(
'操作成功!'); //提示信息 emit('submit'); //提示刷新数据 closeDialog(); //重置窗口状态 } else{
$u.error(
'操作失败');
}
}
});
}

编辑界面的效果如下所示。

3)导入界面的处理

导入界面,一般我们分为几个步骤,一个是提供导入模板下载,然后上传文件并显示数据,然后确认提交即可。

由于导入数据的逻辑上大致类似,不同的是他们的业务数据和验证规则,因此我们通过自定义组件的方式,来简化相关的处理。

我通过改造ele-import 的第三方组件,让它支持Vue3+Typescript语法,实现对业务数据的上传操作。

<template>
  <divclass="main">
    <!--条件及列表展示-->
    <Searchref="searchRef"@show-import="showImport"@show-add="showAdd"@show-view="showView"@show-edit="showEdit" />
    <import-dataref="importRef"@finish="finishImport" />
  </div>
</template>

而在import.vue页面里面,我们的代码是使用ele-import来处理即可。

<template>
  <div>
    <!--模板导入信息-->
    <ele-import:fields="importForm.fields":filepath="importForm.filepath":append="importForm.append":formatter="importForm.formatter":rules="importForm.rules":tips="importForm.tips":title="importForm.title":visible="isVisible":request-fn="saveImport"@close="close"@finish="finishImport" />
  </div>
</template>

通过传入对应的数据,如导入字段,以及规则,以及下载文件等相关参数,就可以实现了文件的上传处理了。

//请求服务端处理上传数据,返回一个Promise对象
const saveImport = async (data) =>{//console.log(data);
  const result =await menu.SaveImport(data);//console.log(result);
  if(result) {returnPromise.resolve();
}
else{returnPromise.reject();
}
};

同时,这也是一个弹出窗口,因此也需要暴露show方法给外部调用。

//显示窗口
const show = () =>{
isVisible.value
= true; //显示对话框 };//暴露组件属性和方法 defineExpose({
show,
});

而这个组件的相关数据信息,定义在importForm中,我们来看看对应的数据格式。

const importForm =reactive({//Excel 模板导入数据
  //弹出层标题
  title: '功能菜单导入',//提示信息
  tips: [], //['商品编号 必填', '产品类型 必填', '商品名称 必填'],
  //字段名称参照表
fields: {//字段根据需要裁剪
    pid: '父ID',
name:
'显示名称',
icon:
'图标',
seq:
'排序',
url:
'Url地址',
webIcon:
'菜单图标',
systemType_ID:
'系统编号',
tag:
'特殊标签',
},
//附加数据, 在每条记录上都会加这两个字段和值 append: {//company: '广州爱奇迪', //leader: '伍华聪' },//参数校检, 和 element-ui 中 form表单中传递的rules一样, 都是使用的 async-validator 库 //https://element.eleme.cn/#/zh-CN/component/form#biao-dan-yan-zheng rules: {
pid: { type:
'string', required: true, message: '父ID必填'},
name: { type:
'string', required: true, message: '显示名称必填'},
url: { type:
'string', required: true, message: 'Url地址必填'},
webIcon: { type:
'string', required: true, message: '菜单图标必填'},
systemType_ID: { type:
'string', required: true, message: '系统编号必填'},
tag: { type:
'string', required: true, message: '特殊标签必填'},
},
//Excel模板下载地址。注意, 只能是.xlsx的文件, .xls或者.cvs都会报错 filepath: 'http://localhost:5043/UploadFiles/template/功能菜单.xlsx',
});

整个导入模块,会通过这个对象的数据格式,进行不同的显示和校验处理等操作。界面效果如下所示。

其中第一步提示信息,并提供模板文件下载

第二步提供文件上传处理。

第三步确认数据并完成处理。

这种统一的操作,我们通过封装,隔离了逻辑步骤的协调处理,只需要在业务组件中生成相关的数据即可,便于使用。

2、利用代码生成工具快速生成

通过上面的介绍,我们了解到整个页面的组件代码结构,因此可以利用它们和数据表之间的关系,生成对应的页面组件,利用代码生成工具Database2Sharp强大的数据库元数据和模板引擎,我们构建了对应的框架代码生成规则,因此统一生成即可,提高了代码开发的效能,同时也统一了代码的结构,便于大项目的维护。

对于SQLSugar的项目框架,我们为了方便,分别单独提供后端代码和Web API代码的生成、Winform界面代码的生成,以及前面介绍到的Vue3+TypeScript+ElementPlus的代码生成操作。

代码生成工具的界面效果如下所示,通过入口菜单,可以实现不同部分的代码快速生成。

通过隔离页面组件的内容变化,实现变化不同通过数据库表关系生成,固定部分采用规定模板预置内容,实现了代码的快速生成操作。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在一个项目中,客户要求对报表中的签名进行仿手写的签名处理,因此我们原先只是显示相关人员的姓名的地方,需要采用手写方式签名,我们的报表是利用FastReport处理的,在利用楷体处理的时候,开发展示倒是正常效果,不过实际上在服务器运行的时候,出来的确实正规的宋体格式,相应的字体都已经安装,不过还是没有生效。因此采用变通的方式,在对应签名的地方采用图片的格式显示,实际效果达到要求。本篇随笔介绍这个过程,利用FastReport传递图片参数,在报表上展示签名信息的处理。

1、报表上的设计处理

例如我们要在报表落款的附近记录相关人员的名字,因此采用签名的显示方式会比较合理。

因此设计相关的报表,本来想是采用文本的方式,变化字体的方式来快捷实现的

不过在报表是在服务器上生成图片的方式,导致正常出来的图片,却没有变化字体,导致出来的是正常的宋体格式。

因此考虑使用图片的格式方式来处理。在其中放置Picture控件,如下所示。

调整好Picture控件的高度和宽度,让它在设计的空白上合适的展示即可。

为了实现图片格式的显示,我们需要在报表的图片控件的BeforePrint事件中解析数据(来自传递参数),数据格式为Base64字符串(从Byte数据转换),如下代码所示。

namespaceFastReport
{
public classReportScript
{
private void shopDoctorImg_BeforePrint(objectsender, EventArgs e)
{
var img = GetImage("ShopDoctor");if(img != null){
shopDoctorImg.Image
=img;
}
}
private void tiaopeiImg_BeforePrint(objectsender, EventArgs e)
{
var img = GetImage("Tiaopei");if(img != null){
tiaopeiImg.Image
=img;
}
}
private void CheckDoctorImg_BeforePrint(objectsender, EventArgs e)
{
var img = GetImage("CheckDoctor");if(img != null){
CheckDoctorImg.Image
=img;
}
}
private void CheckPharmacistImg_BeforePrint(objectsender, EventArgs e)
{
var img = GetImage("CheckPharmacist");if(img != null){
CheckPharmacistImg.Image
=img;
}
}
private Image GetImage(stringsignName)
{
Image img
= null;string imgStr = (string)Report.GetParameterValue(signName);if(!string.IsNullOrEmpty(imgStr))
{
byte[] imgData=Convert.FromBase64String(imgStr);using(MemoryStream ms = newMemoryStream(imgData))
{
img
=System.Drawing.Image.FromStream(ms);
}
}
returnimg;
}

}
}

其中主要注意的是,我们传递的图片数据需要采用Base64String的格式才能正常传递和展示。

2、报表传递图片数据

完成了报表的设计处理,我们剩下的就是在实际的报表中传递对应的参数数据了。

我们把签名图片,放在相对的目录上,如下所示。

然后编写一个公用的读取图片为Base64String的函数处理,如下所示。

        //通过姓名获取签名图片的Base64
        private string GetSignImage(stringsignName)
{
var result = "";string imagePath = Path.Combine(baseDir, $"Report/signs/{signName}.png");if(File.Exists(imagePath))
{
var stream =FileUtil.FileToStream(imagePath);var image =FileUtil.StreamToBytes(stream);if (image != null)
{
result
=Convert.ToBase64String(image);
}
}
returnresult;
}

接着就是根据对应的报表进行加载,并设置相关的参数进行传递给报表即可,如下测试代码所示。

    //生成PDF报表文档到具体文件
    Report report = newReport();
report.Load(reportFile);
//定义参数和数据格式 var dict = new Dictionary<string, object>();#region 测试数据源dict.Add("Name", "张三");
dict.Add(
"Gender", "");
dict.Add(
"Age", 32);
dict.Add(
"Telephone", "18620292076");
dict.Add(
"CreateTime", "2019-10-13 22:30:15");
dict.Add(
"CheckDoctor", GetSignImage("张医生"));//"张医生" dict.Add("CheckPharmacist", GetSignImage("张医生")); //"李药师" dict.Add("SendUser", "王小姐");
dict.Add(
"QrCode", "http://www.iqidi.com");
dict.Add(
"BarCode", "1234567890");//图片文件 dict.Add("ShopDoctor", GetSignImage("张医生"));
dict.Add(
"Tiaopei", GetSignImage("张医生"));
dict.Add(
"Fayao", GetSignImage("王小姐"));#endregionreport.RegisterData(dt,"Detail");foreach (string key indict.Keys)
{
report.SetParameterValue(key, dict[key]);
}
//运行报表 report.Prepare();

由于我们的报表,最终是生成PDF或者图片的方式,方便客户进行在线查询的,因此可以选择PDF或者图片的格式生成。

    //运行报表
report.Prepare();//导出PDF报表//PDFExport export = new PDFExport();//多个图片导出
    int count = 1;string firstFileName = exportImgPath.Replace(".png", "");foreach (PageBase item inreport.Pages)
{
string fileName = string.Format("{0}_{1}.png", firstFileName, count);
exportImgPath
=fileName;//Resolution= 300可以提高分辨率 report.Export(new ImageExport() { PageRange = PageRange.Current, CurPage = count, Resolution= 300}, fileName);
count
++;
}

最后生成的图片格式如下所示,顺利吧签名的图片贴在对应的单元格中即可。

工作流是集成系统的模块应用,使用权限管理系统的身份认证登录后即可使用。本篇随笔介绍工作流模块的界面功能效果。

1、我的审批工作

我的审批工作,包括【我发起的】、【我的待办】、【我的已办】几类大项,并可以根据查询条件进行过滤列表,界面如下所示:

双击申请单,可以打开查看详细的申请单信息,如下界面所示。

如果审批单可以进行审批处理,则会出现相关的审批按钮,单击【审批】按钮,弹出审批对话框,可以录入相关的审批意见,如下界面所示。

如果表单需要撤销,单击【撤销】按钮,会弹出申请单撤销的确认界面,在其中录入撤销意见确认即可。

如果需了解申请单的流程日志信息,单击【流程日志】按钮,弹出申请单的相关日志信息,如下界面所示。

2、业务受理列表

业务受理列表展示当前工作流模块中可以创建的申请表单入口,如下界面所示。

我们可以根据左侧树列表的业务分类进行过滤,展示不同分类的申请单。单击其中图形按钮,可以创建一个新的申请单,申请单界面如下所示。

有些是简单的单表信息,有些则是主从表的明细表单,不同的表单界面,需要在开发模式进行开发处理。

3、我的草稿

我的草稿是保存用户尚未完成的申请单,在用户编辑申请单的时候,可以暂存在草稿里面,方便下次继续完成填写工作。我的草稿列表界面如下所示。

双击草稿列表的申请单,可以打开申请单的填写界面,如下所示。

4、已办业务查询

已办业务查询,按照不同的业务分类,展示相关申请单的列表,列表界面如下所示。

双击列表记录,可以查看具体的申请单详细信息。

5、流程模板

前面介绍过的工作流的业务受理列表,展示的是可以创建申请单的图形按钮列表,如下界面所示的效果列表。

其中的数据就是在这个流程模板中配置管理的,流程模板中管理工作流业务中可以使用的申请单配置信息,流程模板界面如下所示。

其中详细的配置窗体界面如下所示,这里配置框架处理中所需的相关参数数据,以及配置的流程节点。

单击【流程设置】,可以设置该申请单模板的流程步骤,如下界面所示。

6、流程环节管理

流程环节管理,是定义系统工作流中用到的流程环节类型(处理类型)信息,如下界面所示。

其中有常规的审批、归档、会签、阅办等操作,工作流需要根据这些流程类型来显示不同的处理界面。双击记录,可以进行编辑修改处理。

7、所有申请单

所有申请单列出工作流系统中的所有流程表单的信息,便于管理员针对性的查找申请单信息进行调整处理。

双击申请单列表,可以打开具体的申请单进行查看详细信息,如下所示。

8、流程审批操作

对于一个流程处理操作,我们知道一般有审批通过、拒绝、退回到某步骤、转发到内部阅读、阅读,以及包括起草者能撤销表单呢等操作,当然如果还有一些具体的业务,可能还会有一些流程的处理才操作,不过基本上也可以归结为上面几种,只是他们每步处理的数据内容不同而已。因此审批的操作步骤分类如下所示。

这些操作我们都可以通过一些界面操作的封装实现,因为他们基本上都是通用的,我们传入一些流程
ID等相关标识后,就能交给这些标准的操作界面完成了。

如审批界面如下所示,里面包含了通过、拒绝,跳回到某步骤,增加步骤等。

上面的界面是审批过程中,对于某一个流程处理人员实现的操作,而有时候,我们可能需要针对多个人进行某个步骤的处理,如会签处理,会签处理处理界面效果如下所示。

以及传递给内部人员进行分阅操作,那么就应该选定多个人员进行处理,大概的处理界面效果如下所示。

当然,若申请人的申请单填写错误,需要撤销的话,那么也应该有这个操作,撤销表单后,就可以重新填写表单,然后再次提交进行流程。

9、流程会签处理

会签是指创建一个或多个子流程供相关人员进行审批,等待全部人员完成处理后再次回到主流程上,然后决定是否继续流转到下一个流程步骤上去,一般的申请单的主流程如下所示。

这里设置的会签处理就是其中一个步骤,一旦会签处理步骤发起会签,就会构建多个可供审批的子流程了,如下所示。

在会签发起的步骤,指定参与具体流程会签审批的人员,然后流程则会流转到不同人员进行相关的处理【待办事项】。

我在工作流中定义会签完成后,由会签发起人审核(会签结果审核),决定是否进入下一步流程,在审核过程中决定如何处理这个申请单。

在流程定义里面,我们创建一个会签的流程步骤,我们以请假单为例,加入我们要求请假需要由各组长会签通过,然后在继续下面的部门审批、总经理审批步骤,如下所示。

增加会签后的流程步骤如下所示。

完成后可以在流程步骤列表中看到会签的步骤了,如下所示。

我们创建一个请假申请单,用来发起会签处理,介绍会签的步骤说明。

完成请假单后提交给相关处理人,处理人员在待办事项中查看申请单,如下界面所示。

那么他会发起【发起会签】的处理操作,把相关的投票权发送给各个组长进行会签处理。

会签发起后,各个参与会签的人员在【待办事项】里面处理会签意见,如下所示。

各个待审批的人员进行处理后,最后返回给会签发起人决定是否进入下一步流程,如下所示。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

Winform界面中,有时候,需要对界面进行一些多语言的处理,以适应客户场景的使用,如有时候需要考虑英文、日文、韩文等客户的使用,那么在Winform界面如何实现这种多语言的正常处理呢,本篇基于Winform开发框架的基础上介绍使用多语言的处理过程。

1、多语言的处理思路

在国际化环境下,越来越多的程序需要做多语言版本,以适应各种业务需求的变化。在Winform应用程序中实现多语言也有常规的处理方式处理,不过需要针对每个语言版本,重新修改Winform界面的显示,对一些常规的辅助类,也需要引入一个统一的资源管理类来处理多语言的问题,相对比较繁琐。

我们的Winform开发框架,主要为了简化多语言的处理,编写了一些辅助的处理类库,读取对应的JSON多语言对键值对应文件,实现多语言的切换处理。

我们在开始编写程序的时候,使用我们熟悉的中文,然后在百度翻译(
https://fanyi.baidu.com/
)或者Google翻译对应的内容,存储在对应的目录中。

程序运行的时候,加载对应目录json文件即可实现多语言的处理。

首先我们开发系统的时候,需要创建一些多语言的对应目录,具体的目录我们遵循约定规则即可。

为了便于我们的处理,我们以中文为键,其中JSON里面对应的中文和其他语言的翻译结果,如下面是日文的参考内容。

我们记得把JSON文件,始终复制到目录上或者如果较新则复制,

这个目录就是会输出到debug或者Release的运行目录中,我们就是根据相对于运行目录进行资源读取即可,所有模块共用同一的多语言文件,我们可以把各个模块基础通用的多语言文件放在Basic.json文件中,也可以根据模块独立起名。

实际上目录名称是为了区分而已,程序加载的时候,会把目录下面所有的JSON文件进行加载,读取里面的键值作为资源的字典参照。

我们以我们常规的母语开发,即使我们不做多语言,也不影响代码的正常处理,我们只需要把窗体上和代码里面的中文提取出来,然后进行多语言处理(如变为英文/韩文/日文)即可。

2、多语言的处理效果

为了便于体验多语言的切换实际效果,我这里编写了一些测试的案例,并准备了中文、英文、日文、韩文的对照JSON文件,默认界面效果如下。

切换到英文的时候,界面如下所示。

控件英文界面效果

其他英文测试界面

切换韩文界面效果如下所示

其他视图界面效果

日文界面效果如下。

为了更好的测试其他控件的多语言处理,我们编写了一些控件的展示界面如下GridControl,TreeList等控件。

效果都没有问题,来回测试测试也是正常,符合要求。

3、多语言的代码处理

看完实际的效果,那么我们需要如何在代码中处理,才能实现多余的正常切换呢。

首先我们为了记住用户选择的语言区域信息,我们在配置文件中增加一个键值用来保存区域代码。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <addkey="CultureInfo"value="en-US" />
  </appSettings>

然后切换语言的时候,编写处理语言的切换和区域的存储即可,如下所示。

        /// <summary>
        ///改变界面的语言/// </summary>
        /// <param name="lang"></param>
        private void ChangeLang(stringlang)
{
if (lang == "cn")
{
System.Threading.Thread.CurrentThread.CurrentUICulture
= new System.Globalization.CultureInfo("zh-Hans");
config.AppConfigSet(
"CultureInfo", "zh-Hans");
}
else if (lang == "kr")
{
System.Threading.Thread.CurrentThread.CurrentUICulture
= new System.Globalization.CultureInfo("ko-KR");//韩文界面 config.AppConfigSet("CultureInfo", "ko-KR");
}
else if (lang == "en")
{
System.Threading.Thread.CurrentThread.CurrentUICulture
= new System.Globalization.CultureInfo("en-US");//英文界面 config.AppConfigSet("CultureInfo", "en-US");
}
else if (lang == "jp")
{
System.Threading.Thread.CurrentThread.CurrentUICulture
= new System.Globalization.CultureInfo("ja-JP");//日文界面 config.AppConfigSet("CultureInfo", "ja-JP");
}

LanguageHelper.Reload();
//重新初始化JSON资源 LanguageHelper.InitLanguage(this, true);//刷新界面控件 }

上面的
LanguageHelper
辅助函数,主要就是用来对界面进行多语言处理的。

为了方便,我们的封装的基类窗体BaseForm、BaseEditForm、BaseDock等基础窗体,我们一般在界面初始化的时候,都会调用
LanguageHelper
辅助函数来进行界面的多语言处理。

因此,如果是采用我们的基类窗口,那么默认就带有多语言的处理功能了,我们按照约定规则,处理好对应的多语言翻译文件JSON文件即可。

如果是对于一些自定义的窗体,或者继承默认的窗体类,那么在构造函数完成处理的时候,可以增加一行代码来处理多语言,如上面的规则所示即可。

这样也就能实现多语言的处理了

在一个应用系统的开发框架中,往往很多地方需要用到缓存的处理,有些地方是为了便于记录用户的数据,有些地方是为了提高系统的响应速度,如有时候我们在发送一个短信验证码的时候,可以在缓存中设置几分钟的过期时间,这样验证短信验证码的时候,就会自动判断是否过期了。本篇随笔结合CSRedis的使用,介绍如何实现缓存的初始化及使用的处理。

1、在基于.netCore的Web API后端使用CSRedis

关于CSRedis的使用,我们可以参考Github网站:
https://github.com/2881099/csredis
进行了解。

首先我们在使用前,需要添加对应的程序集应用。

Package Name NuGet Downloads
CSRedisCore nuget stats
Caching.CSRedis nuget stats IDistributedCache

CSRedisCore是必须的,而Caching.CSRedis则是在用到分布式缓存的时候需要用到。

初始化CSRedis也比较简单,如代码所示。

var csredis = new CSRedis.CSRedisClient("127.0.0.1:6379,password=123,defaultDatabase=13,prefix=my_");

不过我们的Redis配置一般放在appSettings.json文件中,不是直接硬编码的,所以需要调整一下。

//初始化Redis及分布式缓存
var redisConnectionString = builder.Configuration["CSRedis:ConnectString"];
RedisHelper.Initialization(
newCSRedisClient(redisConnectionString));
builder.Services.AddSingleton
<IDistributedCache>(new CSRedisCache(RedisHelper.Instance));

常规的缓存设置,通过键、值、时间设置等几个内容进行处理,如下代码所示。

RedisHelper.Set("test1", "123123", 60);

RedisHelper.Get(
"test1");

如果我们要清空所有的缓存键值,那么对键进行模式匹配进行处理即可。

/// <summary>
///清空Redis缓存/// </summary>
protected voidClearRedisCache()
{
//查找所有分区节点中符合给定模式(pattern)的 key var cacheKeys = RedisHelper.Keys("*");
RedisHelper.Del(cacheKeys);
}

除了常规的缓存处理,redis也支持消息队列的处理,消息队列最熟悉无疑是 rabbitmq,它基本是业界标准的解决方案。另外 redis 也提供了多种实现轻订阅方法。如下面是一案例代码。

//程序1:使用代码实现订阅端
var sub = RedisHelper.Subscribe(("chan1", msg =>Console.WriteLine(msg.Body)));//sub.Disponse();//停止订阅//程序2:使用代码实现发布端
RedisHelper.Publish("chan1", "666666");

我们这里不深究消息队列的处理,有兴趣的可以参考文章《
【由浅至深】redis 实现发布订阅的几种方式
》进行了解即可。

2、前端发送短信验证码及后端判断

我们这里以一个短信验证码登录的前端来介绍CSRedis缓存的设置、获取、移除等操作过程。

例如,我们的移动前端,需要验证码登录系统的时候,需要发送验证码的操作,如下所示。

前端通过初步判断手机号码正确后,可以向后端请求发送验证码,如下逻辑代码所示(vue)

//获取验证码
getCode() {if (this.model.mobile.length < 11 && !uni.$u.test.mobile(this.model.mobile)) {
uni.$u.toast(
'手机号码不正确')return;
}
//发送短信验证码 var params ={
PhoneNumber:
this.model.mobile
}
user.SendPhoneLoginSmsCode(
params).then(res =>{if(res.success) {this.show = false;
uni.$u.toast(`验证码已发送至手机 ${
this.model.mobile},请注意查收!`)

let interval
= setInterval(() =>{this.second--;if (this.second <= 0) {this.show = true;
clearInterval(interval);
}
},
1000);
}
else{
uni.$u.toast(
'发送出现错误:' +res.errorMessage)
}
})
}

WebAPI后端,处理逻辑是构建随机的验证码并通过短信发送到手机上,并缓存好对应的验证码,后端的处理代码如下所示

/// <summary>
///发送登录动态码/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
[Route(
"send-login-smscode")]public async Task<CommonResult>SendPhoneLoginSmsCode(PhoneCaptchaModel model)
{
//获取随机6位数字动态验证码 var code = RandomChinese.GetRandomNumber(6);//使用自定义模板处理短信发送 string message = string.Format(ConfigData.MySmsCodeTemplate, code);var result = await_smsSender.SendAsync(model.PhoneNumber, message);if(result.Success)
{
var cacheKey = model.PhoneNumber;//以手机号码作为键存储验证码缓存 var cacheItem = new SmsLoginCodeCacheItem { Code = code, PhoneNumber =model.PhoneNumber };

RedisHelper.Set(cacheKey, cacheItem, TimeSpan.FromMinutes(ConfigData.SmsCodeExpiredMinutes));
//获取的时候//var tmp = RedisHelper.Get<SmsLoginCodeCacheItem>(cacheKey); }returnresult;
}

顺利发送短信验证码后,前端会提示用户验证码发送情况,并要求输入验证码进行登录,前端登录的代码如下所示。

//短信验证码登录
loginByCode() {var params ={
mobile:
this.model.mobile,
smscode:
this.model.code
};
console.log(params);
user.dynamiclogin(params)
.then(res
=>{
uni.$u.toast(
'验证成功');this.gotoPage();
})
.
catch(error =>{
console.log(
'验证失败' +error);
uni.$u.toast(error);
});
},

后端的登录处理,主要就是通过在Redis中读取对应的手机验证码,如果匹配进行令牌的生成处理,否则提示错误信息。

/// <summary>/// 登录授权处理/// </summary>/// <returns></returns>
[AllowAnonymous]
[HttpPost]
[Route(
"authenticate-byphone")]
public async Task
<AuthenticateResultDto>AuthenticateByPhoneCaptcha(PhoneCaptchaModel model)
{
var authResult = newAuthenticateResultDto();
#region 条件检查
if(string.IsNullOrEmpty(model.PhoneNumber))
{
throw new MyApiException("手机号不能为空");
}
if(string.IsNullOrEmpty(model.SmsCode))
{
throw new MyApiException("验证码不能为空");
}
var userInfo = await _userService.GetFirstAsync(s => s.MobilePhone ==model.PhoneNumber);if (userInfo == null)
{
throw new MyApiException("用户手机不存在");
}
#endregion
var cacheKey = model.PhoneNumber;//以手机号码作为键存储验证码缓存 var item = RedisHelper.Get<SmsLoginCodeCacheItem>(cacheKey);if (item != null && item.Code ==model.SmsCode)
{
//根据用户身份生成tokenresult authResult.AccessToken = GenerateToken(userInfo); //令牌 authResult.Expires = expiredDays * 24 * 3600; //失效秒数 authResult.Success = true;//成功 authResult.UserId = userInfo.Id;//当前用户Id //移除缓存短信键值 RedisHelper.Del(cacheKey);
}
else{
authResult.Error
= "登录失败,无法生成令牌";
}
returnauthResult;
}

如果顺利生成令牌,则从redis中移除对应的缓存键值即可。

如果我们需要查看Redis的缓存内容,Windows端可以安装 RedisDesktopManager 进行查看管理Redis的内容。

发送短信后,键值会保存在Redis缓存中,可以通过RedisDesktopManager  进行查看。

手机端顺利收到短信提示。

以上就是关于在SqlSugar的开发框架,通过介绍短信验证码的前后端协作方式,介绍使用CSRedis实现缓存的处理过程。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面