在早期一直使用Lumisoft.NET组件来进行邮件的处理查找,对于邮件的处理非常方便,之前在随笔《
基于Lumisoft.NET组件的POP3邮件接收和删除操作
》中也介绍过基于POP3和SMPT进行邮件的收发处理,一般邮件服务器对于Pop3都是支持很好的,常规使用测试多个服务器都没问题,所以就没怎么研究IMAP协议的处理,本篇随笔基于原来POP3 的需求扩展了IMAP协议的处理。

1、创建IMAP收件辅助类

为了方便进行收取IMAP邮件的处理,我们创建一个ImapHelper,并传入相关用到的一些参数,用来封装收件的处理操作。

如下辅助类所示,传入服务器域名地址,端口,是否SSL,用户名和密码等信息。

接下来,我们需要连接服务器,并尝试获取授权信息,如果通过,则可以进行下一步获取邮件信息的操作,如下代码所示。

        /// <summary>
        ///收取邮件操作/// </summary>
        public voidReceive()
{
using (var client = newIMAP_Client())
{
//创建日志处理 client.Logger = newLogger();
client.Logger.WriteLog
+= new EventHandler<WriteLogEventArgs>(WriteLog);//响应记录显示//使用账号密码,连接服务器 client.Connect(server, port, useSsl);//登录获取授权操作 client.Login(username, password);//var identity = client.AuthenticatedUserIdentity;//获取各个邮箱目录的概要信息 client.GetFolders(null).ToList().ForEach(f =>{

Console.WriteLine(f.FolderName);
var s =client.FolderStatus(f.FolderName);
s.ToList().ForEach(sIt
=>{
Console.WriteLine(
"总数:{0},未读:{1},最近{2}", sIt.MessagesCount, sIt.MessagesCount, sIt.UnseenCount);
});

});

我们登录获得授权后,测试获取各个目录的概要邮件信息,如总邮件数量,以及未读数量等等。

然后通过选择具体的邮箱目录,并设置返回信息包含的内容格式,以及从服务器返回那些序号的邮件等等,如下代码所示。

    //选择邮箱
    client.SelectFolder("INBOX");//首先确定取第x到第n封邮件,"1:*"表示第1封到最后一封
    var seqSet = IMAP_t_SeqSet.Parse("1:*");var items = newIMAP_t_Fetch_i[]
{
new IMAP_t_Fetch_i_Envelope(), //邮件的标题、正文等信息 new IMAP_t_Fetch_i_Uid(), //返回邮件的UID号,UID号是唯一标识邮件的一个号码 new IMAP_t_Fetch_i_Flags(), //此邮件的标志,应该是已读未读标志 new IMAP_t_Fetch_i_InternalDate(),//貌似是收到的日期 new IMAP_t_Fetch_i_Rfc822() //Rfc822是标准的邮件数据流,可以通过Lumisoft.Net.Mail.Mail_Message对象解析出邮件的所有信息 };

接着我们通过传入条件,并给他一个回调匿名函数处理相关的邮件信息,如下所示。

    //Fetch 第一个参数false时seqSet有效
    client.Fetch(false, seqSet, items, (s, e) =>{//处理邮件的匿名函数内容
    });

接着我们处理邮件信息的转换,吧邮件信息转换为
Mail_Message
对象的信息,这个包含邮件相关的头部信息,正文,以及附件信息等全部内容。

   var email = e.Value asIMAP_r_u_Fetch;if (email.Rfc822 != null)
{
email.Rfc822.Stream.Position
= 0;var mime_message =Mail_Message.ParseFromStream(email.Rfc822.Stream);
email.Rfc822.Stream.Close();

然后我们把邮件的信息进一步转换为我们需要存储在数据库的对象信息,最后写入数据库即可。

    receiveInfo.ReceivedDate = DateTime.Now;//接收本地时间
    receiveInfo.Company_ID = this.companyId;
receiveInfo.User_ID
= this.userId;
receiveInfo.Email
= this.email;//接收Email账号 receiveInfo.MailConfig_ID = this.mailConfig_ID;//接收Email账号的配置记录ID//每封Email会有一个在Pop3服务器范围内唯一的Id,检查这个Id是否存在就可以知道以前有没有接收过这封邮件 receiveInfo.MailUid =email.UID.UID.ToString();try{//可能会出现【LumiSoft.Net.ParseException: Header field 'Date' parsing failed】异常错误。 receiveInfo.SendDate =mime_message.Date;
}
catch(Exception ex)
{
receiveInfo.SendDate
= Convert.ToDateTime("1900-1-1");//错误赋值一个日期 error = string.Format("转换邮件的Date出错:账号{0} 邮件标题:{1}", username, mime_message.Subject);
LogTextHelper.Error(error, ex);
}
//可能出现乱码问题,通过函数进行转换 receiveInfo.Title = mime_message.Subject;//DecodeString(mime_header.Subject); receiveInfo.MailBody=mime_message.BodyText;try{if (!string.IsNullOrEmpty(mime_message.BodyHtmlText))
{
receiveInfo.MailBody
=mime_message.BodyHtmlText;
}
}
catch{//屏蔽编码出现错误的问题,错误在BodyText存在而BodyHtmlText不存在的时候,访问BodyHtmlText会出现 }

写入数据库处理,调用我们通用处理类处理数据信息的存储即可。

    #region 写入邮件信息到数据库
    int mailId = -1;try{
mailId
= BLLFactory<MailReceive>.Instance.Insert2(receiveInfo);
}
catch(Exception ex)
{
error
= string.Format("写入邮件信息到数据库出错:账号{0} 邮件标题:{1}", username, mime_message.Subject);
LogTextHelper.Error(error, ex);
}
if (mailId <= 0) return; //如果邮件没有保存,不要保存附件 #endregion

2、邮件的附件处理

邮件的附件,包含常规的邮件附件,以及嵌入正文的附件图片,因此需要进行不同类型的判断,并一起把附件获取下来存储,这样在显示的时候,才能正常显示相关的附件。

其中
Mail_Message
对象有一个函数,可以获取全部这两类附件的信息到列表中。

public MIME_Entity[] GetAttachments(bool includeInline, bool includeEmbbedMessage)

这样我们来调用这个函数,然后进行附件的提取存储处理即可。

    #region 邮件附件内容
    foreach (var entity in mime_message.GetAttachments(true, true))
{
string fileName = "";#region 判断是普通附件还是嵌入的内容附件 if (entity.ContentDisposition != null &&entity.ContentDisposition.DispositionType==MIME_DispositionTypes.Attachment)
{
Console.WriteLine(
"Attachment:" +entity.ContentDisposition.Param_FileName);
fileName
=entity.ContentDisposition.Param_FileName;
}
else{string cid = entity.ContentID.Substring(1, entity.ContentID.Length - 2);if (entity.ContentType.Param_Name != null &&mime_message.BodyHtmlText.Contains(string.Format("cid:{0}", cid)))
{
Console.WriteLine(
"Embeded image:" +cid);
fileName
=cid;
}
else{
Console.WriteLine(
"Unknown attachment.");
}
}

邮件的附件信息,entity对象需要转换为MIME_b_SinglepartBase进行处理的。

 var byteObj = entity.Body as MIME_b_SinglepartBase;

因此我们可以通过文件方式存储它的字节数据,如下所示。

File.WriteAllBytes(filename, byteObj.Data);

或者调用附件信息进行存储处理(可以是本地存储、或者FTP上传等方式)

如对于测试带有嵌入图片,附件信息的邮件,这样处理能够顺利获取所有的附件信息。

因此可以使用邮件管理模块中的定时收发邮件的处理,实现邮件的接收和发送。

3、163邮箱对于IMAP协议不支持

在测试IMAP协议收取邮件的时候,对于POP3发现大多数邮箱都是支持的。

但虽然163邮箱对POP3的支持不错,对IMAP协议却不支持,都是使用授权码进行登录,也确实登录成功了,但是IMAP协议切换邮箱进行邮件收取的时候,就会提示

提示错误信息。

00023 NO SELECT Unsafe Login. Please contact kefu@188.com for help

如有兴趣,了解Lumisoft.NET组件的相关使用内容,请参考我相关随笔,谢谢。


基于Lumisoft.NET组件和.NET API实现邮件发送功能的对比


基于Lumisoft.NET实现的邮件发送功能


基于Lumisoft.NET组件开发碰到乱码等一些问题的解决


基于Lumisoft.NET组件的SMTP账号登陆检测


邮件代收代发软件操作说明


邮件代收代发功能模块的操作界面设计和阶段性总结

在我的各种开发框架中,数据访问有的基于微软企业库,有的基于EFCore的实体框架,两者各有其应用场景,不过多的去比较。最近在使用SqlSugar的时候,觉得这个数据访问处理的组件确实很灵活,据说性能也是很不错,于是用来整合测试一下,它对多种关系型数据库如SqlServer、Oracle、Mysql、SQLite、PostgreSQL都很容易提供支持,通过特性标注的方式,可以很好的实现数据访问的处理,这点很类似EfCore的处理,同时SqlSugar又提供很灵活的SQL处理操作。因此多花了一些时间,把SqlSugar的数据访问操作进行一定的封装处理,最后使之适应更广泛的应用。在这个处理过程中,我编写一些单元测试用来测试其相关功能,并编写了几个模块的Winform界面进行测试,效果还是很不错,借此总结分享一下。

1、SQLSugar的相关介绍

SqlSugar是一款 老牌 .NET 开源ORM框架,由果糖大数据科技团队维护和更新 ,使用文档可以参考官方文档:
https://www.donet5.com/Home/Doc
, GitHub的地址是:
https://github.com/donet5/SqlSugar

优点: 简单易用、功能齐全、高性能、轻量级,支持数据库:MySql、SqlServer、Sqlite、Oracle 、 postgresql、达梦、人大金仓。

由于它是ORM的框架组件,因此标识性的处理也是常规的操作,一般的SQLSugar对数据实体对象提供和数据库信息的标注处理。

如对于数据库表的标注:

[SugarTable("TB_DictData")]public classDictDataInfo
{
}

以及对字段信息主键的标注

        /// <summary>
        ///编号/// </summary>
        [SugarColumn(IsPrimaryKey= true)]public virtual string ID { get; set; }

或者是自增字段的标注处理

    public classPerson 
{
//数据库字段 [SqlSugar.SugarColumn(IsPrimaryKey =true,IsIdentity=true)]public int Id { get; set; }

而有些字段,和数据库字段是没有对应关系的,可以设置忽略标识,如下所示。

    public classPerson 
{
//数据库字段 [SqlSugar.SugarColumn(IsPrimaryKey =true,IsIdentity =true)]public int Id { get; set; }public int SexId { get; set; }//非数据库字段 [SqlSugar.SugarColumn(IsIgnore =true)]public string SexName { get; set; }

.......

定义了这些实体和数据库关系后,我们操作数据库,可以使用 SqlSugarClient 或者 SqlSugarScope 对数据库进行增、删、查、改等功能,SqlSugarClient 和SqlSugarScope 几乎一样,两者差异之处,是后者使用单例(单件)模式,如果我们的对象也是单件模式,就考虑使用SqlSugarScope 对象操作数据库。

例如我们创建一个SqlSugarClient的对象实例,用它来操作数据库获取信息。

var db = new SqlSugarClient(newConnectionConfig()
{
DbType
=DbType.SqlServer,
ConnectionString
=connectionString,
InitKeyType
=InitKeyType.Attribute,
IsAutoCloseConnection
= true,
AopEvents
= newAopEvents
{
OnLogExecuting
= (sql, p) =>{
Log.Information(sql);
Log.Information(
string.Join(",", p?.Select(it => it.ParameterName + ":" +it.Value)));
}
}
});

那接下来,我们就可以利用db来进行数据的增删改查处理操作了。

     //查询表的所有
     var list = db.Queryable<Student>().ToList();//插入
     db.Insertable(new Student() { SchoolId = 1, Name = "jack"}).ExecuteCommand();//更新
     db.Updateable(new Student() { Id = 1, SchoolId = 2, Name = "jack2"}).ExecuteCommand();//删除
     db.Deleteable<Student>().Where(it => it.Id == 1).ExecuteCommand();

一般来说,我们可能倾向于把操作封装为一个函数处理,如下所示

/// <summary>
///保存数据到数据库/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<bool>SaveData(LongVoiceResultDto dto)
{
bool result = false;if(dto != null)
{
using(var db =CreateDb())
{
var info = newConsultationInfo();
info.DiscernStatus
=dto.taskId;
info.OperateStatus
= "未识别";if (dto.data != null && dto.data.speechResult != null)
{
if (dto.data.statusCode == 3)
{
info.OperateStatus
= "已识别";
}
var speechResult =dto.data.speechResult;
info.DiscernText
=speechResult.resultText;
}

result
= await db.Insertable(info).ExecuteCommandAsync() > 0;
}
}
returnresult;
}

从上面的代码来看,我们定义好实体信息后,就可以直接用SqlSugarClient的对象实例来处理数据库信息了,过程非常简单高效,特别对于一些简单的单表操作,非常简洁。

2、SQLSugar的基类封装

上面的简单代码,我们可以看到SqlSugarClient的对象实例的快捷操作数据库操作,非常方便。

不过一般来说,对于一个成熟的项目,我们一般是要尽可能的重用一些处理代码,并提供最大程度的简化封装。因此我们在实际使用来开发项目的时候,需要对 SqlSugar数据库的处理进行一定的封装操作,以期最大程度的优化代码。

首先我们定义一个对象用来承载数据库SqlSugarScope(或者SqlSugarClient)实例的信息,用于数据访问的基类上下文方便使用的目的。

    /// <summary>
    ///数据库上下文信息/// </summary>
    public classDbContext
{
/// <summary> ///数据库类型。/// </summary> public DbType DbType { get; set; }/// <summary> ///连接字符串。/// </summary> public string ConnectionString { get; set; }/// <summary> ///数据库类型。/// </summary> public SqlSugarScope Client { get; set; }publicDbContext()
{
//默认采用配置项名//appSettings/DefaultDb 配置项为指定连接字符串的name var dbConfigName = ConfigurationManager.AppSettings["DefaultDb"];
Init(dbConfigName);
}
public DbContext(stringdbConfigName)
{
Init(dbConfigName);
}

我们为了方便配置不同的数据库信息,因此通过定义一个默认的键 DefaultDb 来确定具体使用那个连接字符串。如下是我们的数据库连接字符串。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <!--Sqlserver数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI" />
    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=winframework;Uid=root;Pwd=123456;SslMode=none" />
    <!--sqlite数据库字符串,路径符号|DataDirectory|代表当前运行目录-->
    <addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WinFramework.db;Version=3;" />
    <!--PostgreSQL数据库的连接字符串-->
    <addname="npgsql"providerName="Npgsql"connectionString="Server=localhost;Port=5432;Database=winframework;User Id=postgres;Password=123456" />
    <!--不受驱动影响,32位64位均可使用-->
    <addname="oracle"providerName="OracleManaged"connectionString="Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=win;Password=win" />
    <!--达梦数据库的连接字符串-->
    <addname="Dm"providerName="Dm"connectionString="Server=localhost;User ID=SYSDBA;PWD=SYSDBA;Database=WINFRAMEWORK;" />
  </connectionStrings>
  
  <appSettings>
    <!--指定默认的数据库类型,如果不指定则使用第一个连接字符串-->
    <addkey="DefaultDb"value="sqlserver" />
  </appSettings>
  <startup>
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.8" />
  </startup>
</configuration>

其中我们通过连接字符串中的 providerName 的类别来确定具体使用那种数据库类型。

       /// <summary>
        ///根据链接字符串的providerName决定那种数据库类型/// </summary>
        /// <param name="setting"></param>
        /// <returns></returns>
        privateDbType GetSugarDbType(ConnectionStringSettings setting)
{
DbType dbType
= DbType.SqlServer; //默认值 var providerName =setting.ProviderName;if (providerName != null)
{
//数据库providerName:SqlClient MySqlClient SQLite OracleManaged/OracleClient Npgsql if (providerName.EndsWith(".SqlClient", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.SqlServer;
}
else if (providerName.EndsWith(".MySqlClient", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.MySql;
}
else if (providerName.EndsWith(".SQLite", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Sqlite;
}
else if (providerName.EndsWith("OracleManaged", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Oracle;
}
else if (providerName.EndsWith(".OracleClient", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Oracle;
}
else if (providerName.EndsWith("Npgsql", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.PostgreSQL;
}
else if (providerName.EndsWith("Dm", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Dm;
}
}
returndbType;
}

这样我们就可以动态设置数据库的配置信息了,我们可以使用配置信息,初始化数据库操作实例的代码逻辑。

数据库上下文对象处理好后,我们就来设计我们的数据库操作基类对象了,基类对象需要基于实体信息来定义一些常规的CRUD接口,并应最大程度的提供一些重写或者设置处理。

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public class MyCrudService<TEntity, TKey, TGetListInput>
        where TEntity : class, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///数据库上下文信息/// </summary> protectedDbContext dbContent;/// <summary> ///简化SugarClient 的 ADO对象/// </summary> protectedIAdo Ado
{
get{returndbContent.Client.Ado;
}
}
/// <summary> ///实体对象处理类/// </summary> protected SimpleClient<TEntity>EntityDb
{
get{return dbContent.Client.GetSimpleClient<TEntity>();
}
}
/// <summary> ///数据库配置名称,默认为空。///可在子类指定不同的配置名称,用于访问不同的数据库/// </summary> public string DbConfigName { get; set; }publicMyCrudService()
{
dbContent
= newDbContext();
}

我们看到基类提供一些SqlSugarClient对象的应用,以方便对数据的处理操作。

我们看看获取所有,以及根据Lamda条件表达式获取列表的操作代码,非常方便的。

        /// <summary>
        ///获取所有记录/// </summary>
        public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
var list = awaitEntityDb.GetListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}
/// <summary> ///根据条件,获取所有记录/// </summary> public virtual async Task<ListResultDto<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> input, string orderBy = null)
{
var query =EntityDb.AsQueryable().Where(input);
query
= query.OrderByIF(!string.IsNullOrEmpty(orderBy), orderBy);var list = awaitquery.ToListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}

由于本身的SqlSugarClient/SqlSugarScope提供了很多接口函数,因此我们的基类只需要在它的基础上进行一些简单的封装即可,如删除处理代码。

        /// <summary>
        ///删除指定ID的对象/// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool>DeleteAsync(TEntity input)
{
return awaitEntityDb.DeleteAsync(input);
}
/// <summary> ///根据指定条件,删除集合/// </summary> /// <param name="input"></param> /// <returns></returns> public virtual async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>>input)
{
var result = awaitEntityDb.DeleteAsync(input);returnresult;
}
/// <summary> ///删除指定ID的对象/// </summary> /// <param name="id">记录ID</param> /// <returns></returns> public virtual async Task<bool>DeleteByIdAsync(TKey id)
{
return awaitEntityDb.DeleteByIdAsync(id);
}
/// <summary> ///删除集合/// </summary> /// <param name="input">删除条件集合</param> /// <returns></returns> public async virtual Task<bool> DeleteByIdsAsync(IEnumerable<TKey>input)
{
dynamic ids =input.ToArray();return awaitEntityDb.DeleteByIdsAsync(ids);
}

上面删除,可以根据实体类,Lamda条件表达式,主键或者主键列表等,简单封装一下就可以了。

根据相关的数据操作需要,我们为该基类定义很多常规通用的基类接口,包含很多常规的CRUD等的方法,列出一个列表方便参考即可。

3、SQLSugar数据访问的单元测试

为了对不同数据库类型的不同操作进行检查,看其是否能够正常工作,我们需要编写一些测试的代码用于检查我们基类函数封装的有效性,只有对每一个基类接口进行测试了,才能够放心的使用。

为了编写单元测试,我们需要为几个表编写对应的实体类和相应的服务类(继承自SQLSugar的数据访问基类),我们可以使用
代码生成工具Database2Sharp
来快速生成实体类代码,如下所示。

生成代码直接显示在代码工具上,可以复制下来使用。

后面有空会调整一下代
码生成工具Database2Sharp
,把SQLSugar的ORM实体类和基于CRUD基类的服务类一并生成代码出来就完美了(和其他项目开发一样,快速生成项目代码即可)。

完成了实体类信息的处理后,我们来继承一下基类服务类并重写查询条件处理和列表排序的函数即可,如下代码所示。

   /// <summary>
    ///应用层服务接口实现/// </summary>
    public class DictDataService : MyCrudService<DictDataInfo, string, DictDataPagedDto>{/// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override ISugarQueryable<DictDataInfo>CreateFilteredQueryAsync(DictDataPagedDto input)
{
var query = base.CreateFilteredQueryAsync(input);

query
=query
.WhereIF(
!input.Name.IsNullOrWhiteSpace(), t =>t.Name.Contains(input.Name))
.WhereIF(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIF(
!string.IsNullOrEmpty(input.Value), t => t.Value ==input.Value)
.WhereIF(
!string.IsNullOrEmpty(input.DictType_ID), t => t.DictType_ID ==input.DictType_ID);returnquery;
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<DictDataInfo> ApplySorting(ISugarQueryable<DictDataInfo>query, DictDataPagedDto input)
{
return base.ApplySorting(query, input).OrderBy(s => s.DictType_ID).OrderBy(s =>s.Seq);//先按第一个字段排序,然后再按第二字段排序//return base.ApplySorting(query, input).OrderBy(s=>s.DictData_ID).OrderBy(s => s.Seq); }
}

其中 CreateFilteredQueryAsync 代码是重写构建查询条件处理的逻辑,而ApplySorting函数用于指定列表的排序规则。

有了代码生成工具的辅助,因此我们编写一些单元测试函数用于测试,编写单元测试也是非常方便的事情。

代码的单元测试,编写如下所示。

[TestClass]public classUnitTest1
{
/// <summary> ///测试查找记录/// </summary> /// <returns></returns> [TestMethod]public async Task TestMethod1()

创建单元测试项目,并指定测试类为[Testclass]以及测试方法[TestMethod]即可,测试方法我们根据实际要求编写覆盖所有方法的测试即可。

例如我对于测试返回列表和单体数据的接口,编写单元代码如下所示。

[TestClass]public classUnitTest1
{
/// <summary> ///测试查找记录/// </summary> /// <returns></returns> [TestMethod]public asyncTask TestMethod1()
{
var input = newDictTypePagedDto()
{
Name
= "客户"};//可以使用BLLFactory工厂类处理 var service = BLLFactory<DictTypeService>.Instance;//new DictTypeService(); var count = await service.CountAsync(s=> true);
Assert.AreNotEqual(
0, count);var list = awaitservice.GetAllAsync();
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);

list
= awaitservice.GetListAsync(input);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);var ids = list.Items.Select(s => { return s.ID; }).Take(2);
list
= awaitservice.GetAllByIdsAsync(ids);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);var id = list.Items[0].ID;var info = awaitservice.GetAsync(id);
Assert.IsNotNull(info);
Assert.AreEqual(id, info.ID);

info
= await service.GetFirstAsync(s => true);
Assert.IsNotNull(info);
awaitTask.CompletedTask;
}

测试增删改查的接口的单元测试代码如下所示。

        /// <summary>
        ///测试增删改查/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod2()
{
var info = newDictTypeInfo()
{
ID
=Guid.NewGuid().ToString(),
Code
= "test",
Name
= "test",
Remark
= "test",
PID
= "-1",
Seq
= "001"};var service = newDictTypeService();var insert = awaitservice.InsertAsync(info);
Assert.IsTrue(insert);

info.Name
= "test2";var update = awaitservice.UpdateAsync(info);
Assert.IsTrue(update);
var deleted = awaitservice.DeleteByIdAsync(info.ID);
Assert.IsTrue(deleted);
var entity = awaitservice.GetAsync(info.ID);
Assert.IsNull(entity);
}

测试对SQL语句执行过程的单元测试代码如下

       /// <summary>
        ///测试执行语句的处理/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod3()
{
var service = newDictTypeService();var sql = string.Format("Select * from TB_DictType");var table =service.SqlTable(sql);
Assert.IsNotNull(table);
Assert.IsTrue(table.Rows.Count
> 0);var ds =service.SqlDataSet(sql);
Assert.IsNotNull(ds);
Assert.IsTrue(ds.Tables.Count
> 0);

sql
= string.Format("Select Name from TB_DictType");var list =service.SqlValueList(sql);
Assert.IsNotNull(list);
//完全没有执行任何更新、插入,返回-1 var result =service.SqlExecute(sql);
Assert.IsTrue(result
== -1);awaitTask.CompletedTask;
}

测试数据库参数化及多数据库切换处理的单元测试代码如下所示。

       /// <summary>
        ///测试数据库参数化及多数据处理/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod4()
{
var service = newDictTypeService();var sql = string.Format("Select * from TB_DictType Where PID = @pid");var parameters = new List<SugarParameter>() { new SugarParameter("pid", "-1") };//默认SQLServer数据库 var table =service.SqlTable(sql, parameters);
Console.WriteLine(table.Rows.Count);
Assert.IsNotNull(table);
Assert.IsTrue(table.Rows.Count
> 0);//切换到MySQL数据库 service.SetDbConfigName("mysql");var list =service.SqlQuery(sql, parameters);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);//切换到SQLITE数据库 service.SetDbConfigName("sqlite");var list2 =service.SqlQuery(sql, parameters);
Assert.IsNotNull(list2);
Assert.IsNotNull(list2.Items);
Assert.IsTrue(list2.Items.Count
> 0);//切换到npgsql数据库 service.SetDbConfigName("npgsql");var list3 =service.SqlQuery(sql, parameters);
Assert.IsNotNull(list3);
Assert.IsNotNull(list3.Items);
Assert.IsTrue(list3.Items.Count
> 0);awaitTask.CompletedTask;
}

在开发机器上安装几个不同的关系数据库,用于测试,并准备好数据库文件导入。

在单元测试项目中右键运行测试,如下图所示。

全部测试通过,这几个单元测试覆盖了我们的所有方法的测试了。

以上就是我们基于SqlSugar的ORM处理的封装,并提供了丰富的基类接口和弹性化的实体类泛型约束,因此 子类代码非常简洁,只需要实现条件查询和排序的处理即可,因此代码生成的时候,会更加简便。

通过上面的预演,我们基本上了解了SqlSugar的使用操作,确实非常方便,特别是我们基于代码生成工具的辅助开发之后,会更加省事快捷,使用基于强大的CRUD基类,我们子类的代码更少,更容易维护。

而对于一些多表关联的操作,我们可以在子类额外定义一些处理函数即可。

在上篇随笔《
基于SqlSugar的数据库访问处理的封装,支持多数据库并使之适应于实际业务开发中
》中介绍了SqlSugar的基础用法,以及实现对常规项目中对数据访问的基类封装,并通过编写单元测试覆盖相关的功能测试,虽然最后编写单元测试的代码就是实际调用数据处理的代码,不过没有界面不太直观,本篇随笔继续深入SqlSugar的使用介绍,介绍基于Winform项目界面的整合测试。

1、数据访问层的实现

在上篇随笔,我们介绍了SqlSugar使用起来还是非常简单的,首先定义好和数据表对应的实体类信息,通过特性声明给的方式,声明表名和字段信息(包括主键信息)

如对于数据库表的标注:

[SugarTable("TB_DictData")]
public class DictDataInfo
{
}

以及对字段信息主键的标注

        /// <summary>
        ///编号/// </summary>
        [SugarColumn(IsPrimaryKey= true)]public virtual string ID { get; set; }

或者是自增字段的标注处理

    public classPerson 
{
//数据库字段 [SugarColumn(IsPrimaryKey=true,IsIdentity=true)]public int Id { get; set; }

例如我们对于Winform开发框架中的字典数据库,设计关系如下所示。

我们生成器对应的SQLSugar实体信息如下所示,这些枯燥的工作可以交给配套的代码生成工具Database2sharp来完成。

    /// <summary>
    ///DictTypeInfo/// </summary>
    [SugarTable("TB_DictType")]public classDictTypeInfo
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicDictTypeInfo()
{
this.ID =System.Guid.NewGuid().ToString();this.LastUpdated =System.DateTime.Now;

}
#region Property Members[SugarColumn(IsPrimaryKey= true)]public virtual string ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典代码/// </summary> public virtual string Code { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }/// <summary> ///编辑者/// </summary> public virtual string Editor { get; set; }/// <summary> ///编辑时间/// </summary> public virtual DateTime LastUpdated { get; set; }/// <summary> ///分类:0 客房/1 KTV/2 茶室/// </summary> public virtual string PID { get; set; }#endregion}

同时为了方便条件的分页处理,我们定义一个分页的Dto对象,如下所示。

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public classDictTypePagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
{
/// <summary> ///默认构造函数/// </summary> public DictTypePagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skip ///">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public DictTypePagedDto(int skipCount, intresultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public DictTypePagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
#region Property Members /// <summary> ///不包含的对象的ID,用于在查询的时候排除对应记录/// </summary> public virtual string ExcludeId { get; set; }public virtual string Name { get; set; }public virtual string Code { get; set; }public virtual string Remark { get; set; }public virtual string Seq { get; set; }public virtual string PID { get; set; }/// <summary> ///创建时间-开始/// </summary> public DateTime? CreationTimeStart { get; set; }/// <summary> ///创建时间-结束/// </summary> public DateTime? CreationTimeEnd { get; set; }#endregion}

同理对于字典项目的实体信息,也是类似的定义方式,如下所示。

    /// <summary>
    ///DictDataInfo/// </summary>
    [SugarTable("TB_DictData")]public classDictDataInfo
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicDictDataInfo()
{
this.ID =System.Guid.NewGuid().ToString();this.LastUpdated =System.DateTime.Now;

}
#region Property Members /// <summary> ///编号/// </summary> [SugarColumn(IsPrimaryKey = true)]public virtual string ID { get; set; }/// <summary> ///字典大类/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }/// <summary> ///编辑者/// </summary> public virtual string Editor { get; set; }/// <summary> ///编辑时间/// </summary> public virtual DateTime LastUpdated { get; set; }#endregion}

最终我们定义完成实体信息后,需要集成上篇随笔提到的数据访问基类,并重写一下查询条件处理,排序的规则信息即可,如下代码所示。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class DictTypeService : MyCrudService<DictTypeInfo, string, DictTypePagedDto>{/// <summary>
        ///获取字段中文别名(用于界面显示)的字典集合/// </summary>
        /// <returns></returns>
        public override Task<Dictionary<string, string>>GetColumnNameAliasAsync()
{
var dict = new Dictionary<string, string>();#region 添加别名解析dict.Add("ID", "编号");
dict.Add(
"Name", "类型名称");
dict.Add(
"Code", "字典代码");
dict.Add(
"Remark", "备注");
dict.Add(
"Seq", "排序");
dict.Add(
"Editor", "编辑者");
dict.Add(
"LastUpdated", "编辑时间");
dict.Add(
"PID", "父ID");#endregion returnTask.FromResult(dict);
}
/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<DictTypeInfo>CreateFilteredQueryAsync(DictTypePagedDto input)
{
var query = base.CreateFilteredQueryAsync(input);
query
=query
.WhereIF(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.ID != input.ExcludeId) //不包含排除ID .WhereIF(!string.IsNullOrEmpty(input.Code), t => t.Code ==input.Code)
.WhereIF(
!string.IsNullOrEmpty(input.PID), t => t.PID ==input.PID)
.WhereIF(
!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals .WhereIF(!input.Remark.IsNullOrWhiteSpace(), t => t.Remark.Contains(input.Remark)) //如需要精确匹配则用Equals .WhereIF(!input.Seq.IsNullOrWhiteSpace(), t => t.Seq.Contains(input.Seq)) //如需要精确匹配则用Equals//创建日期区间查询 .WhereIF(input.CreationTimeStart.HasValue, s => s.LastUpdated >=input.CreationTimeStart.Value)
.WhereIF(input.CreationTimeEnd.HasValue, s
=> s.LastUpdated <=input.CreationTimeEnd.Value)
;
returnquery;
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<DictTypeInfo> ApplySorting(ISugarQueryable<DictTypeInfo>query, DictTypePagedDto input)
{
return base.ApplySorting(query, input).OrderBy(s =>s.Seq);
}
}

其中
MyCrudService
采用了泛型的定义方式,传入对应的实体类,主键类型,以及排序分页的对象DTO等,方便基类实现强类型的接口处理。

这个子类我们也可以通过代码生成的方式实现批量生成即可。

整合到项目里面,把实体类和数据访问的服务类区分不同的目录放置,便于管理即可。

2、Winform界面的开发和调用数据操作处理

上面我们完成了数据库表的实体类和对应数据访问服务类的处理后,我们接下来的就是设计Winform界面用来处理相关的数据处理。

我这里把我的基于微软企业库访问模式的Winform界面部分拷贝过来调整一下,如下界面所示。

查看和编辑字典大类界面

编辑字典项目

对于数据访问类的调用,我们使用了一个工厂类来创建对应的单例应用,如下获取字典大类列表。

        /// <summary>
        ///绑定树的数据源/// </summary>
        private asyncTask BindTree()
{
var pageDto = newDictTypePagedDto();var result = await BLLFactory<DictTypeService>.Instance.GetListAsync(pageDto);if (result != null)
{
this.tree.DataSource =result.Items;this.tree.ExpandAll();
}
}

而但我们单击某个字典大类的时候,应该列出对应大类下的字典项目,因此获取字典项目的数据操作如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<DictDataInfo>> GetData(stringdictType)
{
//构建分页的条件和查询条件 var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
{
DictType_ID
=dictType
};
var result = await BLLFactory<DictDataService>.Instance.GetListAsync(pagerDto);//new DictDataService().GetListAsync(pagerDto); returnresult;
}

我们这里使用了分页查询的条件
DictDataPagedDto
,如果是需要获取全部,我们也可以通过调用GetAllAsync()函数来实现,如下导出全部的时候代码如下所示。

        private async void winGridViewPager1_OnStartExport(objectsender, EventArgs e)
{
if (this.winGridViewPager1.IsExportAllPage)
{
var result = await BLLFactory<DictDataService>.Instance.GetAllAsync();this.winGridViewPager1.AllToExport =result.Items;
}
}

这些处理都是基类预先定义好的API,我们通过子类强类型传入即可,非常方便,也简化很多代码。

同样,我们可以通过Get接口获取指定ID的实体信息,如下所示。

            if (!string.IsNullOrEmpty(ID))
{
var info = await BLLFactory<DictDataService>.Instance.GetAsync(ID);if (info != null)
{
this.txtName.Text =info.Name;this.txtNote.Text =info.Remark;this.txtSeq.Text =info.Seq;this.txtValue.Text =info.Value;
}
}

在Winform编辑界面中,我们重写保存更新的代码如下所示。

        public override async Task<bool>SaveUpdated()
{
var info = await BLLFactory<DictDataService>.Instance.GetAsync(ID);if (info != null)
{
SetInfo(info);
try{return await BLLFactory<DictDataService>.Instance.UpdateAsync(info);
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
return false;
}

以上是Winform界面中对常规数据处理接口的调用,这些都是通过强类型实体的方式调用基类函数,非常方便快捷,同时以提供了很好的API统一性实现。

最终界面效果和原先Winform开发框架一样功能。

Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样. 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js。简单地说,只要你会Moment.js,那么你就会Day.js!

但是我们知道的,Moment.js 的大小是200多KB,而Day.js的大小却是2 KB,瘦身很多但却几乎拥有同样强大的 API。

现在框架基本上都是多端应用的了,所以在ABP框架中整合Winform管理端、Vue&element的BS前端,以及公司动态网站用于发布产品和网站信息等都是常见的应用,有时候,我们还需要根据功能的需要,增加一些小程序的支持,这些对于Web API后端来说,都是很容易接入的应用处理。

而基于Vue + Element 的前端界面,前端功能模块包括用户管理、组织机构管理、角色管理、菜单管理、功能管理及权限分配,日志管理、字典管理、产品管理等管理功能,可实现用户的功能及数据权限进行控制管理。

公司动态门户网站,有时候用于我们发布网站信息和产品信息的一个门户网站,采用了Bootstrap-Vue界面组件,由于大多数门户网站都是基于Bootstrap栅格系统的,因此基于最新Bootstrap-Vue界面组件也就是最佳选择的了,而且可以重用很多Bootstrap的网站模板案例。这样也同时保持了前端模块同时也是基于Vue的,摒弃了以前基于JQuery的繁琐操作DOM处理。

由于现在微信小程序的广泛应用,有时候我们针对一些业务模块功能,可以根据需要推出一些小程序应用场景,这些对接我们的统一授权系统,以及统一的WebAPI调用机制即可满足。

而这些基于Vue的前端,采用类库对日期格式化,或者进行转换的需求场景很多,因此我们我们需要借助JS类库进行日期的处理,比较好的推荐使用day.js的处理。

1、day.js的安装和使用

安装day.js

npm install dayjs -s

然后在项目代码中引入即可:

import dayjs from "dayjs"; //导入日期js
或者var dayjs = require('dayjs')
import * as dayjs from 'dayjs'import* as isLeapYear from 'dayjs/plugin/isLeapYear' //导入插件
import 'dayjs/locale/zh-cn' //导入本地化语言
dayjs.extend(isLeapYear)//使用插件
dayjs.locale('zh-cn') //使用本地化语言

详细了解可以参考地址:
https://dayjs.fenxianglu.cn/
或者
https://dayjs.gitee.io/zh-CN/
或者
https://github.com/iamkun/dayjs

Day.js 支持几十个国家语言,但除非手动加载,多国语言默认是不会被打包到工程里。但你可以随意在各个语言之间自由切换:

dayjs('2018-05-05').locale('zh-cn').format() //在这个实例上使用简体中文

直接调用
dayjs()
将返回一个包含当前日期和时间的 Day.js 对象。

var now = dayjs()

可以对调用dayjs对格式进行处理或者转换。

dayjs("12-25-1995", "MM-DD-YYYY")
dayjs('2018 三月 15', 'YYYY MMMM DD', 'zh-cn')

dayjs('1970-00-00', 'YYYY-MM-DD').isValid() // true
dayjs('1970-00-00', 'YYYY-MM-DD', true).isValid() // false
dayjs('1970-00-00', 'YYYY-MM-DD', 'es', true).isValid() // false

默认情况下,Day.js 只包含核心的代码,并没有安装任何插件.

您可以加载多个插件来满足您的需求,例如官方提供的插件就有:

  • AdvancedFormat 扩展了 dayjs().format API 以支持更多模版
  • RelativeTime 增加了 .from .to .fromNow .toNow 4个 API 来展示相对的时间 (e.g. 3 小时以前).
  • IsLeapYear 增加了 .isLeapYear API 返回一个 boolean 来展示一个 Dayjs's 的年份是不是闰年.
  • WeekOfYear 增加了 .week() API 返回一个 number 来表示 Dayjs 的日期是年中第几周.
  • IsSameOrAfter 增加了 .isSameOrAfter() API 返回一个 boolean 来展示一个时间是否和一个时间相同或在一个时间之后.
  • IsSameOrBefore 增加了 .isSameOrBefore() API 返回一个 boolean 来展示一个时间是否和一个时间相同或在一个时间之前.

Day.js 支持像这样的链式调用:

dayjs().add(7, 'day')
dayjs(
'2019-01-25').add(1, 'day').subtract(1, 'year').year(2009).toString()

Day.js 对象还有很多查询的方法。

Is Before表示 Day.js 对象是否在另一个提供的日期时间之前。

dayjs().isBefore(dayjs('2011-01-01')) //默认毫秒
dayjs().isBefore('2011-01-01', 'year')

Is Same 检查一个 Dayjs 对象是否和另一个 Dayjs 对象时间相同。

Is After
表示 Day.js 对象是否在另一个提供的日期时间之后。

还有其他查询时间区间的函数,如下所示。

如下面一个应用代码函数:

    /**
* 当前日期gantt状态
* row: object 当前行信息
* date: string 当前格子日期
* unit: string 时间单位,以天、月、年计算
*/dayGanttType(row, date, unit= "days") {
let start_date
=row.startTime;
let end_date
=row.endTime;
let between
=dayjs(date).isBetween(start_date, end_date, unit);if(between) {return "item-on";
}
let start
=dayjs(start_date).isSame(date, unit);
let end
=dayjs(end_date).isSame(date, unit);if (start &&end) {return "item-on item-full";
}
if(start) {return "item-on item-start";
}
if(end) {return "item-on item-end";
}
},

2、Moment.js的安装和使用

Moment.js是一个轻量级的JavaScript时间库,它方便了日常开发中对时间的操作,提高了开发效率。 moment.js作为日期处理工具,虽然它和day.js对比显得有点笨重,不过依旧很多项目在广泛的使用者。

npm的安装如下处理。

npm install moment --save

实例代码


var now = moment(); //取当前时间
now.format('YYYY-MM-DD'); //格式化输出
var day = moment("9/12/2010 19:05:25", "MM/DD/YYYY HH

在一些内部OA或者流转的文件,或者给一些客户的报价文件、合同,或者一些医院出示的给保险机构的病历资料等,有时候可能都希望快速的使用电子签章的处理方式来给文件盖上特定的印章,本篇随笔介绍基于Vue&Element的前端项目采用第三方组件

vue-drag-resize和图片转换Base64的方式实现图片印章的盖章处理。

1、图片转换为Base64处理

图片转换为Base64编码可以通过在线工具的转换方式实现图片转Base64编码,网上很多在线的处理,百度一下即可。
如:
https://c.runoob.com/front-end/59/
或者
https://tool.chinaz.com/tools/imgtobase
试过都可以,非常方便。
如果我们喜欢通过C#代码进行图片的转换也可以使用自己封装的函数实现处理,如下所示。

Base64 在CSS中的使用

.demoImg{ background-image: url("data:image/jpg;base64,/9j/4QMZRXhpZgAASUkqAAgAAAAL...."); }

Base64 在HTML中的使用

<imgwidth="40"height="30"src="data:image/jpg;base64,/9j/4QMZRXhpZgAASUkqAAgAAAAL...." />

我们使用C#代码转换的处理代码如下所示。

        private void btnBase64_Click(objectsender, EventArgs e)
{
var base64 = ImageHelper.ImageToBase64Str(this.pictureBox1.Image);if(chkData.Checked)
{
base64
= "data:image/jpeg;base64," +base64;
}
this.txtBase64.Text =base64;
}

而其中调用是通过我们公用类库中的图片辅助类进行,详细转换代码如下所示。

        /// <summary>
        ///从文件中转换图片对象到Base64编码/// </summary>
        /// <param name="imageFilePath">图片文件路径</param>
        /// <returns></returns>
        public static string ImageToBase64Str(stringimageFilePath)
{
Image image
=Image.FromFile(imageFilePath);using (MemoryStream ms = newMemoryStream())
{
image.Save(ms, image.RawFormat);
//ImageFormat.Jpeg byte[] imageBytes =ms.GetBuffer();string imgBase64Str =Convert.ToBase64String(imageBytes);//释放资源,让别的使用 image.Dispose();returnimgBase64Str;
}
}
/// <summary> ///转换图片对象到Base64编码/// </summary> /// <param name="image">Image图片对象</param> /// <returns></returns> public static stringImageToBase64Str(Image image)
{
using (MemoryStream ms = newMemoryStream())
{
image.Save(ms, image.RawFormat);
//ImageFormat.Jpeg byte[] imageBytes =ms.GetBuffer();string imgBase64Str =Convert.ToBase64String(imageBytes);returnimgBase64Str;
}
}

这样,我们在前端Vue的项目中,就可以赋值这段图片Base64代码到HTML文件中就可以了,如下是前端Vue项目代码所示(缩减了部分Base64编码)。

这样我们就可以在页面中放置一个Base64编码的图片在页面中了。

2、使用vue-drag-resize组件实现印章图片的拖动

常规的图片,放置在页面中,位置是固定的,如果我们需要拖动印章,那么就需要引入可拖动面板的Vue组件vue-drag-resize来处理它了。

vue-drag-resize是Github上的一个开源组件,地址是:
https://github.com/kirillmurashov/vue-drag-resize

这个组件的使用和其他组件的使用方式一样,非常方便。

import Vue from 'vue'import VueDragResize from'vue-drag-resize'Vue.component('vue-drag-resize', VueDragResize)

在组件或者页面中使用的代码如下所示。

import VueDragResize from 'vue-drag-resize'exportdefault{
components: { VueDragResize },

它的HTML代码如下所示。

<template>
    <divid="app">
        <VueDragResize:isActive="true":w="200":h="200"v-on:resizing="resize"v-on:dragging="resize">
            <h3>Hello World!</h3>
            <p>{{ top }} х {{ left }} </p>
            <p>{{ width }} х {{ height }}</p>
        </VueDragResize>
    </div>
</template>

为了把印章图片可以拖动,我们在HTML中放置印章图片包含在这个组件面板中。

如下面代码所示。

把图片放置在这个组件容器中后,图片就可以随意拖动,确认位置后,就可以确定它的位置,我们可以通过记录图片的位置X, Y的值并存储起来,下次直接确定位置也可以。

定义组件的初始X位置。

<vue-drag-resize:x="0">

定义组件的初始Y位置。

<vue-drag-resize:y="0">

例如我们定义了一个报价单,并通过设置,把图片放置在页面中,让使用者可以通过拖动印章的方式,放到合适的位置上去,然后进行打印报价单即可带有印章的报价单出来了。

这样就可以实现电子印章的拖动和放置处理了,记住其位置和状态,下次就可以直接定位到指定的位置上了。

另外,一般文档都需要打印,关于打印的处理,有很多方式,可以使用print.js(
https://github.com/crabbly/Print.js
),也可以使用
vue-print-nb
(
https://github.com/Power-kxLee/vue-print-nb
),甚至复杂的定义可以考虑使用CLODOP组件来处理,不过我们这里可以简单的内置Windows 对象的打印操作方式来处理普通的页面打印即可。

print () {
const print
= this.$refs.print.innerHTML
const printPart
= print +csstyle;
const newTab
= window.open('_blank');
newTab.document.body.innerHTML
=printPart;
newTab.print();
newTab.close();
},

打印效果如下所示,采用了对应的CSS样式处理后,和实际的页面效果相当。

在项目中,涉及到了html内容打印的需求,调用了浏览器的window.print用于打印
显示不全问题

由于window.print是1:1打印,打印内容过宽时,浏览器会自动从左截取掉超宽部分,因此在打印前需将页面进行调整
打印页边距设定为 0mm 时,网页内最大元素的分辨率:794×1123
因此可以将内容div设置为700px,剩余空间设置为页边距

去除浏览器默认页眉页脚

页眉打印默认有页眉页脚信息,展现到页面外边距范围,我们可以通过去除页面模型page的外边距,使得内容不会延伸到页面的边缘,再通过设置 body 元素的 margin 来保证 A4 纸打印出来的页面带有外边距

由于window.print打印自带页眉页脚,用于存放打印url,日期时间,页面名称等内容,为屏蔽这些内容可使用css进行屏蔽

@media print{@page {
margin
:0; }body{margin:1cm; }}

只屏蔽页脚

@page{margin-bottom:0; }
事件监听

有两个事件可以监听到到打印事件,一个是
beforeprint
,一个是
afterprint
,分别表示打印事件触发前后。
这个事件在 IE6 就已经支持了,兼容大概是 Firefox、IE全支持, Chrome63+支持, Safari暂不支持

我们可在处理事件onbeforeprint() 将一些不需要打印的元素隐藏,和打印后的处理事件 onafterprint()放开隐藏的元素

window.addEventListener('beforeprint', ()=>{
document.body.innerHTML
= '打印前触发';
});

window.addEventListener(
'afterprint', ()=>{
document.body.innerHTML
= '打印后触发';
});
设置打印布局(横向、纵向、边距)
@media print{@page {
// 纵向
size
:portrait;// 横向
size
:landscape;// 边距
margin
:0cm 0cm 0cm 0cm; }}

如果需要了解更多Vue&Element的开发知识,可以查看我的随笔分类,里面总结了很多我在实际开发过程中遇到的问题,和经验分享。


循序渐进VUE+Element