smartadmin.core.urf 这个项目是基于asp.net core 3.1(最新)基础上参照领域驱动设计(DDD)的理念,并参考目前最为了流行的abp架构开发的一套轻量级的快速开发web application 技术架构,专注业务核心需求,减少重复代码,开始构建和发布,让初级程序员也能开发出专业并且漂亮的Web应用程序
域驱动设计(DDD)是一种通过将实现与不断发展的模型相连接来满足复杂需求的软件开发方法。域驱动设计的前提如下:
最终的核心思想还是SOLID,只是实现的方式有所不同,ABP可能目前对DDD设计理念最好的实现方式。但对于小项目我还是更喜欢 URF.Core https://github.com/urfnet/URF.Core 这个超轻量级的实现。
同时这个项目也就是我2年前的一个开源项目 ASP.NET MVC 5 SmartCode Scaffolding for Visual Studio.Net 的升级版,支持.net core.目前没有把所有功能都迁移到.net core,其中最重要的就是代码生成这块。再接下来的时间里主要就是完善代码生成的插件。当然也要看是否受欢迎,如果反应一般,我可能不会继续更新。
演示站点 账号:demo 密码:123456
GitHub 源代码 https://github.com/neozhu/smartadmin.core.urf
喜欢请给个 Star 每一颗Star都是鼓励我继续更新的动力 谢谢 如果你用于自己公司及盈利性的项目,希望给与金钱上的赞助,并且保留原作者的版权
smartadmin.core.urf遵行DDD设计模式来实现应用程序的四层模型
域层(Domain Layer)
第一个简单的需求开始 新增 Company 企业信息 完成CRUD 导入导出功能
在SmartAdmin.Entity.csproj项目的Models目录下新增一个Company.cs类
1 //记住:定义实体对象最佳做法,继承基类,使用virtual关键字,尽可能的定义每个属性,名称,类型,长度,校验规则,索引,默认值等
2 namespace SmartAdmin.Data.Models
3 {
4 public partial class Company : URF.Core.EF.Trackable.Entity
5 {
6 [Display(Name = "企业名称", Description = "归属企业名称")]
7 [MaxLength(50)]
8 [Required]
9 //[Index(IsUnique = true)]
10 public virtual string Name { get; set; }
11 [Display(Name = "组织代码", Description = "组织代码")]
12 [MaxLength(12)]
13 //[Index(IsUnique = true)]
14 [Required]
15 public virtual string Code { get; set; }
16 [Display(Name = "地址", Description = "地址")]
17 [MaxLength(128)]
18 [DefaultValue("-")]
19 public virtual string Address { get; set; }
20 [Display(Name = "联系人", Description = "联系人")]
21 [MaxLength(12)]
22 public virtual string Contect { get; set; }
23 [Display(Name = "联系电话", Description = "联系电话")]
24 [MaxLength(20)]
25 public virtual string PhoneNumber { get; set; }
26 [Display(Name = "注册日期", Description = "注册日期")]
27 [DefaultValue("now")]
28 public virtual DateTime RegisterDate { get; set; }
29 }
30 }
31 //在 SmartAdmin.Data.csproj 项目 SmartDbContext.cs 添加
32 public virtual DbSet<Company> Companies { get; set; }
在项目 SmartAdmin.Service.csproj 中添加ICompanyService.cs,CompanyService.cs 就是用来实现业务需求 用例的地方
1 //ICompany.cs
2 //根据实际业务用例来创建方法,默认的CRUD,增删改查不需要再定义
3 namespace SmartAdmin.Service
4 {
5 // Example: extending IService<TEntity> and/or ITrackableRepository<TEntity>, scope: ICustomerService
6 public interface ICompanyService : IService<Company>
7 {
8 // Example: adding synchronous Single method, scope: ICustomerService
9 Company Single(Expression<Func<Company, bool>> predicate);
10 Task ImportDataTableAsync(DataTable datatable);
11 Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc");
12 }
13 }
14 // 具体实现接口的方法
15 namespace SmartAdmin.Service
16 {
17 public class CompanyService : Service<Company>, ICompanyService
18 {
19 private readonly IDataTableImportMappingService mappingservice;
20 private readonly ILogger<CompanyService> logger;
21 public CompanyService(
22 IDataTableImportMappingService mappingservice,
23 ILogger<CompanyService> logger,
24 ITrackableRepository<Company> repository) : base(repository)
25 {
26 this.mappingservice = mappingservice;
27 this.logger = logger;
28 }
29
30 public async Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc")
31 {
32 var filters = PredicateBuilder.FromFilter<Company>(filterRules);
33 var expcolopts = await this.mappingservice.Queryable()
34 .Where(x => x.EntitySetName == "Company")
35 .Select(x => new ExpColumnOpts()
36 {
37 EntitySetName = x.EntitySetName,
38 FieldName = x.FieldName,
39 IgnoredColumn = x.IgnoredColumn,
40 SourceFieldName = x.SourceFieldName
41 }).ToArrayAsync();
42
43 var works = (await this.Query(filters).OrderBy(n => n.OrderBy(sort, order)).SelectAsync()).ToList();
44 var datarows = works.Select(n => new
45 {
46 Id = n.Id,
47 Name = n.Name,
48 Code = n.Code,
49 Address = n.Address,
50 Contect = n.Contect,
51 PhoneNumber = n.PhoneNumber,
52 RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss")
53 }).ToList();
54 return await NPOIHelper.ExportExcelAsync("Company", datarows, expcolopts);
55 }
56
57 public async Task ImportDataTableAsync(DataTable datatable)
58 {
59 var mapping = await this.mappingservice.Queryable()
60 .Where(x => x.EntitySetName == "Company" &&
61 (x.IsEnabled == true || (x.IsEnabled == false && x.DefaultValue != null))
62 ).ToListAsync();
63 if (mapping.Count == 0)
64 {
65 throw new NullReferenceException("没有找到Work对象的Excel导入配置信息,请执行[系统管理/Excel导入配置]");
66 }
67 foreach (DataRow row in datatable.Rows)
68 {
69
70 var requiredfield = mapping.Where(x => x.IsRequired == true && x.IsEnabled == true && x.DefaultValue == null).FirstOrDefault()?.SourceFieldName;
71 if (requiredfield != null || !row.IsNull(requiredfield))
72 {
73 var item = new Company();
74 foreach (var field in mapping)
75 {
76 var defval = field.DefaultValue;
77 var contain = datatable.Columns.Contains(field.SourceFieldName ?? "");
78 if (contain && !row.IsNull(field.SourceFieldName))
79 {
80 var worktype = item.GetType();
81 var propertyInfo = worktype.GetProperty(field.FieldName);
82 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
83 var safeValue = (row[field.SourceFieldName] == null) ? null : Convert.ChangeType(row[field.SourceFieldName], safetype);
84 propertyInfo.SetValue(item, safeValue, null);
85 }
86 else if (!string.IsNullOrEmpty(defval))
87 {
88 var worktype = item.GetType();
89 var propertyInfo = worktype.GetProperty(field.FieldName);
90 if (string.Equals(defval, "now", StringComparison.OrdinalIgnoreCase) && (propertyInfo.PropertyType == typeof(DateTime) || propertyInfo.PropertyType == typeof(Nullable<DateTime>)))
91 {
92 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
93 var safeValue = Convert.ChangeType(DateTime.Now, safetype);
94 propertyInfo.SetValue(item, safeValue, null);
95 }
96 else if (string.Equals(defval, "guid", StringComparison.OrdinalIgnoreCase))
97 {
98 propertyInfo.SetValue(item, Guid.NewGuid().ToString(), null);
99 }
100 else if (string.Equals(defval, "user", StringComparison.OrdinalIgnoreCase))
101 {
102 propertyInfo.SetValue(item, "", null);
103 }
104 else
105 {
106 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
107 var safeValue = Convert.ChangeType(defval, safetype);
108 propertyInfo.SetValue(item, safeValue, null);
109 }
110 }
111 }
112 this.Insert(item);
113 }
114 }
115 }
116
117 // Example, adding synchronous Single method
118 public Company Single(Expression<Func<Company, bool>> predicate)
119 {
120
121 return this.Repository.Queryable().Single(predicate);
122
123 }
124 }
125 }
MVC Controller
1 namespace SmartAdmin.WebUI.Controllers
2 {
3 public class CompaniesController : Controller
4 {
5 private readonly ICompanyService companyService;
6 private readonly IUnitOfWork unitOfWork;
7 private readonly ILogger<CompaniesController> _logger;
8 private readonly IWebHostEnvironment _webHostEnvironment;
9 public CompaniesController(ICompanyService companyService,
10 IUnitOfWork unitOfWork,
11 IWebHostEnvironment webHostEnvironment,
12 ILogger<CompaniesController> logger)
13 {
14 this.companyService = companyService;
15 this.unitOfWork = unitOfWork;
16 this._logger = logger;
17 this._webHostEnvironment = webHostEnvironment;
18 }
19
20 // GET: Companies
21 public IActionResult Index()=> View();
22 //datagrid 数据源
23 public async Task<JsonResult> GetData(int page = 1, int rows = 10, string sort = "Id", string order = "asc", string filterRules = "")
24 {
25 try
26 {
27 var filters = PredicateBuilder.FromFilter<Company>(filterRules);
28 var total = await this.companyService
29 .Query(filters)
30 .AsNoTracking()
31 .CountAsync()
32 ;
33 var pagerows = (await this.companyService
34 .Query(filters)
35 .AsNoTracking()
36 .OrderBy(n => n.OrderBy(sort, order))
37 .Skip(page - 1).Take(rows)
38 .SelectAsync())
39 .Select(n => new
40 {
41 Id = n.Id,
42 Name = n.Name,
43 Code = n.Code,
44 Address = n.Address,
45 Contect = n.Contect,
46 PhoneNumber = n.PhoneNumber,
47 RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss")
48 }).ToList();
49 var pagelist = new { total = total, rows = pagerows };
50 return Json(pagelist);
51 }
52 catch(Exception e) {
53 throw e;
54 }
55
56 }
57 //编辑
58 [HttpPost]
59 [ValidateAntiForgeryToken]
60 public async Task<JsonResult> Edit(Company company)
61 {
62 if (ModelState.IsValid)
63 {
64 try
65 {
66 this.companyService.Update(company);
67
68 var result = await this.unitOfWork.SaveChangesAsync();
69 return Json(new { success = true, result = result });
70 }
71 catch (Exception e)
72 {
73 return Json(new { success = false, err = e.GetBaseException().Message });
74 }
75 }
76 else
77 {
78 var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));
79 return Json(new { success = false, err = modelStateErrors });
80 //DisplayErrorMessage(modelStateErrors);
81 }
82 //return View(work);
83 }
84 //新建
85 [HttpPost]
86 [ValidateAntiForgeryToken]
87
88 public async Task<JsonResult> Create([Bind("Name,Code,Address,Contect,PhoneNumber,RegisterDate")] Company company)
89 {
90 if (ModelState.IsValid)
91 {
92 try
93 {
94 this.companyService.Insert(company);
95 await this.unitOfWork.SaveChangesAsync();
96 return Json(new { success = true});
97 }
98 catch (Exception e)
99 {
100 return Json(new { success = false, err = e.GetBaseException().Message });
101 }
102
103 //DisplaySuccessMessage("Has update a Work record");
104 //return RedirectToAction("Index");
105 }
106 else
107 {
108 var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));
109 return Json(new { success = false, err = modelStateErrors });
110 //DisplayErrorMessage(modelStateErrors);
111 }
112 //return View(work);
113 }
114 //删除当前记录
115 //GET: Companies/Delete/:id
116 [HttpGet]
117 public async Task<JsonResult> Delete(int id)
118 {
119 try
120 {
121 await this.companyService.DeleteAsync(id);
122 await this.unitOfWork.SaveChangesAsync();
123 return Json(new { success = true });
124 }
125
126 catch (Exception e)
127 {
128 return Json(new { success = false, err = e.GetBaseException().Message });
129 }
130 }
131 //删除选中的记录
132 [HttpPost]
133 public async Task<JsonResult> DeleteChecked(int[] id)
134 {
135 try
136 {
137 foreach (var key in id)
138 {
139 await this.companyService.DeleteAsync(key);
140 }
141 await this.unitOfWork.SaveChangesAsync();
142 return Json(new { success = true });
143 }
144 catch (Exception e)
145 {
146 return Json(new { success = false, err = e.GetBaseException().Message });
147 }
148 }
149 //保存datagrid编辑的数据
150 [HttpPost]
151 public async Task<JsonResult> AcceptChanges(Company[] companies)
152 {
153 if (ModelState.IsValid)
154 {
155 try
156 {
157 foreach (var item in companies)
158 {
159 this.companyService.ApplyChanges(item);
160 }
161 var result = await this.unitOfWork.SaveChangesAsync();
162 return Json(new { success = true, result });
163 }
164 catch (Exception e)
165 {
166 return Json(new { success = false, err = e.GetBaseException().Message });
167 }
168 }
169 else
170 {
171 var modelStateErrors = string.Join(",", ModelState.Keys.SelectMany(key => ModelState[key].Errors.Select(n => n.ErrorMessage)));
172 return Json(new { success = false, err = modelStateErrors });
173 }
174
175 }
176 //导出Excel
177 [HttpPost]
178 public async Task<IActionResult> ExportExcel(string filterRules = "", string sort = "Id", string order = "asc")
179 {
180 var fileName = "compnay" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx";
181 var stream = await this.companyService.ExportExcelAsync(filterRules, sort, order);
182 return File(stream, "application/vnd.ms-excel", fileName);
183 }
184 //导入excel
185 [HttpPost]
186 public async Task<IActionResult> ImportExcel()
187 {
188 try
189 {
190 var watch = new Stopwatch();
191 watch.Start();
192 var total = 0;
193 if (Request.Form.Files.Count > 0)
194 {
195 for (var i = 0; i < this.Request.Form.Files.Count; i++)
196 {
197 var model = Request.Form["model"].FirstOrDefault() ?? "company";
198 var folder = Request.Form["folder"].FirstOrDefault() ?? "company";
199 var autosave = Convert.ToBoolean(Request.Form["autosave"].FirstOrDefault());
200 var properties = (Request.Form["properties"].FirstOrDefault()?.Split(','));
201 var file = Request.Form.Files[i];
202 var filename = file.FileName;
203 var contenttype = file.ContentType;
204 var size = file.Length;
205 var ext = Path.GetExtension(filename);
206 var path = Path.Combine(this._webHostEnvironment.ContentRootPath, "UploadFiles", folder);
207 if (!Directory.Exists(path))
208 {
209 Directory.CreateDirectory(path);
210 }
211 var datatable = await NPOIHelper.GetDataTableFromExcelAsync(file.OpenReadStream(), ext);
212 await this.companyService.ImportDataTableAsync(datatable);
213 await this.unitOfWork.SaveChangesAsync();
214 total = datatable.Rows.Count;
215 if (autosave)
216 {
217 var filepath = Path.Combine(path, filename);
218 file.OpenReadStream().Position = 0;
219
220 using (var stream = System.IO.File.Create(filepath))
221 {
222 await file.CopyToAsync(stream);
223 }
224 }
225
226 }
227 }
228 watch.Stop();
229 return Json(new { success = true, total = total, elapsedTime = watch.ElapsedMilliseconds });
230 }
231 catch (Exception e) {
232 this._logger.LogError(e, "Excel导入失败");
233 return this.Json(new { success = false, err = e.GetBaseException().Message });
234 }
235 }
236 //下载模板
237 public async Task<IActionResult> Download(string file) {
238
239 this.Response.Cookies.Append("fileDownload", "true");
240 var path = Path.Combine(this._webHostEnvironment.ContentRootPath, file);
241 var downloadFile = new FileInfo(path);
242 if (downloadFile.Exists)
243 {
244 var fileName = downloadFile.Name;
245 var mimeType = MimeTypeConvert.FromExtension(downloadFile.Extension);
246 var fileContent = new byte[Convert.ToInt32(downloadFile.Length)];
247 using (var fs = downloadFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
248 {
249 await fs.ReadAsync(fileContent, 0, Convert.ToInt32(downloadFile.Length));
250 }
251 return this.File(fileContent, mimeType, fileName);
252 }
253 else
254 {
255 throw new FileNotFoundException($"文件 {file} 不存在!");
256 }
257 }
258
259 }
260 }
MVC Views\Companies\Index
1 @model SmartAdmin.Data.Models.Company
2 @{
3 ViewData["Title"] = "企业信息";
4 ViewData["PageName"] = "Companies_Index";
5 ViewData["Heading"] = "<i class='fal fa-window text-primary'></i> 企业信息";
6 ViewData["Category1"] = "组织架构";
7 ViewData["PageDescription"] = "";
8 }
9 <div class="row">
10 <div class="col-lg-12 col-xl-12">
11 <div id="panel-1" class="panel">
12 <div class="panel-hdr">
13 <h2>
14 公司信息
15 </h2>
16 <div class="panel-toolbar">
17 <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-collapse" data-toggle="tooltip" data-offset="0,10" data-original-title="Collapse"><i class="fal fa-window-minimize"></i></button>
18 <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-fullscreen" data-toggle="tooltip" data-offset="0,10" data-original-title="Fullscreen"><i class="fal fa-expand"></i></button>
19 </div>
20
21 </div>
22 <div class="panel-container show">
23 <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0 text-muted bg-subtlelight-fade ">
24 <div class="row no-gutters align-items-center">
25 <div class="col">
26 <!-- 开启授权控制请参考 @@if (Html.IsAuthorize("Create") -->
27 <div class="btn-group btn-group-sm">
28 <button onclick="append()" class="btn btn-default">
29 <span class="fal fa-plus mr-1"></span> 新增
30 </button>
31 </div>
32 <div class="btn-group btn-group-sm">
33 <button name="deletebutton" disabled onclick="removeit()" class="btn btn-default">
34 <span class="fal fa-times mr-1"></span> 删除
35 </button>
36 </div>
37 <div class="btn-group btn-group-sm">
38 <button name="savebutton" disabled onclick="acceptChanges()" class="btn btn-default">
39 <span class="fal fa-save mr-1"></span> 保存
40 </button>
41 </div>
42 <div class="btn-group btn-group-sm">
43 <button name="cancelbutton" disabled onclick="rejectChanges()" class="btn btn-default">
44 <span class="fal fa-ban mr-1"></span> 取消
45 </button>
46 </div>
47 <div class="btn-group btn-group-sm">
48 <button onclick="reload()" class="btn btn-default"> <span class="fal fa-search mr-1"></span> 查询 </button>
49 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
50 <span class="sr-only">Toggle Dropdown</span>
51 </button>
52 <div class="dropdown-menu dropdown-menu-animated">
53 <a class="dropdown-item js-waves-on" href="javascript:void()"> 我的记录 </a>
54 <div class="dropdown-divider"></div>
55 <a class="dropdown-item js-waves-on" href="javascript:void()"> 自定义查询 </a>
56 </div>
57 </div>
58 <div class="btn-group btn-group-sm hidden-xs">
59 <button type="button" onclick="importExcel.upload()" class="btn btn-default"><span class="fal fa-cloud-upload mr-1"></span> 导入 </button>
60 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split waves-effect waves-themed" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
61 <span class="sr-only">Toggle Dropdown</span>
62 </button>
63 <div class="dropdown-menu dropdown-menu-animated">
64 <a class="dropdown-item js-waves-on" href="javascript:importExcel.downloadtemplate()">
65 <span class="fal fa-download"></span> 下载模板
66 </a>
67 </div>
68 </div>
69 <div class="btn-group btn-group-sm hidden-xs">
70 <button onclick="exportexcel()" class="btn btn-default">
71 <span class="fal fa-file-export mr-1"></span> 导出
72 </button>
73 </div>
74
75 </div>
76
77 </div>
78
79 </div>
80 <div class="panel-content">
81 <div class="table-responsive">
82 <table id="companies_datagrid">
83 </table>
84 </div>
85 </div>
86 </div>
87 </div>
88 </div>
89 </div>
90 <!-- 弹出窗体form表单 -->
91 <div id="companydetailwindow" class="easyui-window"
92 title="明细数据"
93 data-options="modal:true,
94 closed:true,
95 minimizable:false,
96 collapsible:false,
97 maximized:false,
98 iconCls:'fal fa-window',
99 onBeforeClose:function(){
100 var that = $(this);
101 if(companyhasmodified()){
102 $.messager.confirm('确认','你确定要放弃保存修改的记录?',function(r){
103 if (r){
104 var opts = that.panel('options');
105 var onBeforeClose = opts.onBeforeClose;
106 opts.onBeforeClose = function(){};
107 that.panel('close');
108 opts.onBeforeClose = onBeforeClose;
109 hook = false;
110 }
111 });
112 return false;
113 }
114 },
115 onOpen:function(){
116 $(this).window('vcenter');
117 $(this).window('hcenter');
118 },
119 onRestore:function(){
120 },
121 onMaximize:function(){
122 }
123 " style="width:820px;height:420px;display:none">
124 <!-- toolbar -->
125 <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0 text-muted bg-subtlelight-fade sticky-top">
126 <div class="d-flex flex-row-reverse pr-4">
127 <div class="btn-group btn-group-sm mr-1">
128 <button name="saveitembutton" onclick="savecompanyitem()" class="btn btn-default">
129 <i class="fal fa-save"></i> 保存
130 </button>
131 </div>
132 <div class="btn-group btn-group-sm mr-1" id="deleteitem-btn-group">
133 <button onclick="deletecompanyitem()" class="btn btn-danger">
134 <i class="fal fa-trash-alt"></i> 删除
135 </button>
136 </div>
137 </div>
138 </div>
139 <div class="panel-container show">
140 <div class="container">
141 <div class="panel-content">
142 <form id="company_form"
143 class="easyui-form form-horizontal p-1"
144 method="post"
145 data-options="novalidate:true,
146 onChange: function(target){
147 hook = true;
148 $('button[name*=\'saveitembutton\']').prop('disabled', false);
149 },
150 onLoadSuccess:function(data){
151 hook = false;
152 $('button[name*=\'saveitembutton\']').prop('disabled', true);
153 }">
154 @Html.AntiForgeryToken()
155 <!--Primary Key-->
156 @Html.HiddenFor(model => model.Id)
157 <fieldset class="form-group">
158 <!-- begin row -->
159 <!--名称-->
160 <div class="row h-100 justify-content-center align-items-center">
161 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Name)</label>
162 <div class="col-md-4 mb-1 pl-1">
163 <input id="@Html.IdFor(model => model.Name)"
164 name="@Html.NameFor(model => model.Name)"
165 value="@Html.ValueFor(model => model.Name)"
166 tabindex="0" required
167 class="easyui-textbox"
168 style="width:100%"
169 type="text"
170 data-options="prompt:'@Html.DescriptionFor(model => model.Name)',
171 required:true,
172 validType: 'length[0,50]'
173 " />
174 </div>
175 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Code)</label>
176 <div class="col-md-4 mb-1 pl-1">
177 <input id="@Html.IdFor(model => model.Code)"
178 name="@Html.NameFor(model => model.Code)"
179 value="@Html.ValueFor(model => model.Code)"
180 tabindex="1" required
181 class="easyui-textbox"
182 style="width:100%"
183 type="text"
184 data-options="prompt:'@Html.DescriptionFor(model => model.Code)',
185 required:true,
186 validType: 'length[0,12]'
187 " />
188 </div>
189 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Address)</label>
190 <div class="col-md-4 mb-1 pl-1">
191 <input id="@Html.IdFor(model => model.Address)"
192 name="@Html.NameFor(model => model.Address)"
193 value="@Html.ValueFor(model => model.Address)"
194 tabindex="2"
195 class="easyui-textbox"
196 style="width:100%"
197 type="text"
198 data-options="prompt:'@Html.DescriptionFor(model => model.Address)',
199 required:false,
200 validType: 'length[0,50]'
201 " />
202 </div>
203 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Contect)</label>
204 <div class="col-md-4 mb-1 pl-1">
205 <input id="@Html.IdFor(model => model.Contect)"
206 name="@Html.NameFor(model => model.Contect)"
207 value="@Html.ValueFor(model => model.Contect)"
208 tabindex="3"
209 class="easyui-textbox"
210 style="width:100%"
211 type="text"
212 data-options="prompt:'@Html.DescriptionFor(model => model.Contect)',
213 required:false,
214 validType: 'length[0,12]'
215 " />
216 </div>
217 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.PhoneNumber)</label>
218 <div class="col-md-4 mb-1 pl-1">
219 <input id="@Html.IdFor(model => model.PhoneNumber)"
220 name="@Html.NameFor(model => model.PhoneNumber)"
221 value="@Html.ValueFor(model => model.PhoneNumber)"
222 tabindex="4"
223 class="easyui-textbox"
224 style="width:100%"
225 type="text"
226 data-options="prompt:'@Html.DescriptionFor(model => model.PhoneNumber)',
227 required:false,
228 validType: 'length[0,20]'
229 " />
230 </div>
231 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.RegisterDate)</label>
232 <div class="col-md-4 mb-1 pl-1">
233 <input id="@Html.IdFor(model => model.RegisterDate)"
234 name="@Html.NameFor(model => model.RegisterDate)"
235 value="@Html.ValueFor(model => model.RegisterDate)"
236 tabindex="5" required
237 class="easyui-datebox"
238 style="width:100%"
239 type="text"
240 data-options="prompt:'@Html.DescriptionFor(model => model.RegisterDate)',
241 required:true,
242 formatter:dateformatter" />
243 </div>
244 </div>
245 </fieldset>
246 </form>
247 </div>
248 </div>
249 </div>
250 </div>
251
252
253 @await Component.InvokeAsync("ImportExcel", new ImportExcelOptions { entity="Company",
254 folder="Companies",
255 url="/Companies/ImportExcel",
256 tpl="/Companies/Download"
257
258
259 })
260
261 @section HeadBlock {
262 <link href="~/css/notifications/toastr/toastr.css" rel="stylesheet" asp-append-version="true" />
263 <link href="~/css/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.css" rel="stylesheet" asp-append-version="true" />
264 <link href="~/js/easyui/themes/insdep/easyui.css" rel="stylesheet" asp-append-version="true" />
265 }
266 @section ScriptsBlock {
267 <script src="~/js/dependency/moment/moment.js" asp-append-version="true"></script>
268 <script src="~/js/notifications/toastr/toastr.js"></script>
269 <script src="~/js/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.js" asp-append-version="true"></script>
270 <script src="~/js/easyui/jquery.easyui.min.js" asp-append-version="true"></script>
271 <script src="~/js/easyui/plugins/datagrid-filter.js" asp-append-version="true"></script>
272 <script src="~/js/easyui/plugins/columns-ext.js" asp-append-version="true"></script>
273 <script src="~/js/easyui/plugins/columns-reset.js" asp-append-version="true"></script>
274 <script src="~/js/easyui/locale/easyui-lang-zh_CN.js" asp-append-version="true"></script>
275 <script src="~/js/easyui/jquery.easyui.component.js" asp-append-version="true"></script>
276 <script src="~/js/plugin/filesaver/FileSaver.js" asp-append-version="true"></script>
277 <script src="~/js/plugin/jquery.serializejson/jquery.serializejson.js" asp-append-version="true"></script>
278 <script src="~/js/jquery.custom.extend.js" asp-append-version="true"></script>
279 <script src="~/js/jquery.extend.formatter.js" asp-append-version="true"></script>
280 <script>
281 var $dg = $('#companies_datagrid');
282 var EDITINLINE = true;
283 var company = null;
284 var editIndex = undefined;
285 //下载Excel导入模板
286
287 //执行导出下载Excel
288 function exportexcel() {
289 const filterRules = JSON.stringify($dg.datagrid('options').filterRules);
290 console.log(filterRules);
291 $.messager.progress({ title: '请等待',msg:'正在执行导出...' });
292 let formData = new FormData();
293 formData.append('filterRules', filterRules);
294 formData.append('sort', 'Id');
295 formData.append('order', 'asc');
296 $.postDownload('/Companies/ExportExcel', formData).then(res => {
297 $.messager.progress('close');
298 toastr.success('导出成功!');
299 }).catch(err => {
300 //console.log(err);
301 $.messager.progress('close');
302 $.messager.alert('导出失败', err.statusText, 'error');
303 });
304
305 }
306 //弹出明细信息
307 function showdetailswindow(id, index) {
308 const company = $dg.datagrid('getRows')[index];
309 opencompanydetailwindow(company, 'Modified');
310 }
311 function reload() {
312 $dg.datagrid('uncheckAll');
313 $dg.datagrid('reload');
314 }
315 //新增记录
316 function append() {
317 company = {
318 Address: '-',
319 RegisterDate: moment().format('YYYY-MM-DD HH:mm:ss'),
320 };
321 if (!EDITINLINE) {
322 //弹出新增窗口
323 opencompanydetailwindow(company, 'Added');
324 } else {
325 if (endEditing()) {
326 //对必填字段进行默认值初始化
327 $dg.datagrid('insertRow',
328 {
329 index: 0,
330 row: company
331 });
332 editIndex = 0;
333 $dg.datagrid('selectRow', editIndex)
334 .datagrid('beginEdit', editIndex);
335 hook = true;
336 }
337 }
338 }
339 //删除编辑的行
340 function removeit() {
341 if (this.$dg.datagrid('getChecked').length <= 0 && EDITINLINE) {
342 if (editIndex !== undefined) {
343 const delindex = editIndex;
344 $dg.datagrid('cancelEdit', delindex)
345 .datagrid('deleteRow', delindex);
346 hook = true;
347 } else {
348 const rows =$dg.datagrid('getChecked');
349 rows.slice().reverse().forEach(row => {
350 const rowindex =$dg.datagrid('getRowIndex', row);
351 $dg.datagrid('deleteRow', rowindex);
352 hook = true;
353 });
354 }
355 } else {
356 deletechecked();
357 }
358 }
359 //删除该行
360 function deleteRow(id) {
361 $.messager.confirm('确认', '你确定要删除该记录?', result => {
362 if (result) {
363 dodeletechecked([id]);
364 }
365 })
366 }
367 //删除选中的行
368 function deletechecked() {
369 const id =$dg.datagrid('getChecked').filter(item => item.Id != null && item.Id > 0).map(item => {
370 return item.Id;
371 });
372 if (id.length > 0) {
373 $.messager.confirm('确认', `你确定要删除这 <span class='badge badge-icon position-relative'>${id.length} </span> 行记录?`, result => {
374 if (result) {
375 dodeletechecked(id);
376 }
377 });
378 } else {
379 $.messager.alert('提示', '请先选择要删除的记录!', 'question');
380 }
381 }
382 //执行删除
383 function dodeletechecked(id) {
384 $.post('/Companies/DeleteChecked', { id: id })
385 .done(response => {
386 if (response.success) {
387 toastr.error(`成功删除[${id.length}]行记录`);
388 reload();
389 } else {
390 $.messager.alert('错误', response.err, 'error');
391 }
392 })
393 .fail((jqXHR, textStatus, errorThrown) => {
394 $.messager.alert('异常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');
395 });
396 }
397 //开启编辑状态
398 function onClickCell(index, field) {
399
400 company = $dg.datagrid('getRows')[index];
401 const _actions = ['action', 'ck'];
402 if (!EDITINLINE || $.inArray(field, _actions) >= 0) {
403 return;
404 }
405
406 if (editIndex !== index) {
407 if (endEditing()) {
408 $dg.datagrid('selectRow', index)
409 .datagrid('beginEdit', index);
410 hook = true;
411 editIndex = index;
412 const ed = $dg.datagrid('getEditor', { index: index, field: field });
413 if (ed) {
414 ($(ed.target).data('textbox') ? $(ed.target).textbox('textbox') : $(ed.target)).focus();
415 }
416 } else {
417 $dg.datagrid('selectRow', editIndex);
418 }
419 }
420 }
421 //关闭编辑状态
422 function endEditing() {
423
424 if (editIndex === undefined) {
425 return true;
426 }
427 if (this.$dg.datagrid('validateRow', editIndex)) {
428 $dg.datagrid('endEdit', editIndex);
429 return true;
430 } else {
431 const invalidinput = $('input.validatebox-invalid', $dg.datagrid('getPanel'));
432 const fieldnames = invalidinput.map((index, item) => {
433 return $(item).attr('placeholder') || $(item).attr('id');
434 });
435 $.messager.alert('提示', `${Array.from(fieldnames)} 输入有误.`, 'error');
436 return false;
437 }
438 }
439 //提交保存后台数据库
440 function acceptChanges() {
441 if (endEditing()) {
442 if ($dg.datagrid('getChanges').length > 0) {
443 const inserted = $dg.datagrid('getChanges', 'inserted').map(item => {
444 item.TrackingState = 1;
445 return item;
446 });
447 const updated = $dg.datagrid('getChanges', 'updated').map(item => {
448 item.TrackingState = 2
449 return item;
450 });
451 const deleted = $dg.datagrid('getChanges', 'deleted').map(item => {
452 item.TrackingState = 3
453 return item;
454 });
455 //过滤已删除的重复项
456 const changed = inserted.concat(updated.filter(item => {
457 return !deleted.includes(item);
458 })).concat(deleted);
459 //$.messager.progress({ title: '请等待', msg: '正在保存数据...', interval: 200 });
460 $.post('/Companies/AcceptChanges', { companies: changed })
461 .done(response => {
462 //$.messager.progress('close');
463 //console.log(response);
464 if (response.success) {
465 toastr.success('保存成功');
466 $dg.datagrid('acceptChanges');
467 reload();
468 hook = false;
469 } else {
470 $.messager.alert('错误', response.err, 'error');
471 }
472 })
473 .fail((jqXHR, textStatus, errorThrown) => {
474 //$.messager.progress('close');
475 $.messager.alert('异常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');
476 });
477 }
478 }
479 }
480 function rejectChanges() {
481 $dg.datagrid('rejectChanges');
482 editIndex = undefined;
483 hook = false;
484 }
485 $(document).ready(function () {
486 //定义datagrid结构
487 $dg.datagrid({
488 rownumbers: true,
489 checkOnSelect: false,
490 selectOnCheck: false,
491 idField: 'Id',
492 sortName: 'Id',
493 sortOrder: 'desc',
494 remoteFilter: true,
495 singleSelect: true,
496 method: 'get',
497 onClickCell: onClickCell,
498 clientPaging: false,
499 pagination: true,
500 striped: true,
501 filterRules: [],
502 onHeaderContextMenu: function (e, field) {
503 e.preventDefault();
504 $(this).datagrid('columnMenu').menu('show', {
505 left: e.pageX,
506 top: e.pageY
507 });
508 },
509 onBeforeLoad: function () {
510 const that = $(this);
511 document.addEventListener('panel.onfullscreen', () => {
512 setTimeout(() => {
513 that.datagrid('resize');
514 }, 200)
515 })
516 },
517 onLoadSuccess: function (data) {
518 editIndex = undefined;
519 $("button[name*='deletebutton']").prop('disabled', true);
520 $("button[name*='savebutton']").prop('disabled', true);
521 $("button[name*='cancelbutton']").prop('disabled', true);
522 },
523 onCheck: function () {
524 $("button[name*='deletebutton']").prop('disabled', false);
525 },
526 onUncheck: function () {
527 const checked = $(this).datagrid('getChecked').length > 0;
528 $("button[name*='deletebutton']").prop('disabled', !checked);
529 },
530 onSelect: function (index, row) {
531 company = row;
532 },
533 onBeginEdit: function (index, row) {
534 //const editors = $(this).datagrid('getEditors', index);
535
536 },
537 onEndEdit: function (index, row) {
538 editIndex = undefined;
539 },
540 onBeforeEdit: function (index, row) {
541 editIndex = index;
542 row.editing = true;
543 $("button[name*='deletebutton']").prop('disabled', false);
544 $("button[name*='cancelbutton']").prop('disabled', false);
545 $("button[name*='savebutton']").prop('disabled', false);
546 $(this).datagrid('refreshRow', index);
547 },
548 onAfterEdit: function (index, row) {
549 row.editing = false;
550 editIndex = undefined;
551 $(this).datagrid('refreshRow', index);
552 },
553 onCancelEdit: function (index, row) {
554 row.editing = false;
555 editIndex = undefined;
556 $("button[name*='deletebutton']").prop('disabled', true);
557 $("button[name*='savebutton']").prop('disabled', true);
558 $("button[name*='cancelbutton']").prop('disabled', true);
559 $(this).datagrid('refreshRow', index);
560 },
561 frozenColumns: [[
562 /*开启CheckBox选择功能*/
563 { field: 'ck', checkbox: true },
564 {
565 field: 'action',
566 title: '操作',
567 width: 85,
568 sortable: false,
569 resizable: true,
570 formatter: function showdetailsformatter(value, row, index) {
571 if (!row.editing) {
572 return `<div class="btn-group">\
573 <button onclick="showdetailswindow('${row.Id}', ${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="查看明细" ><i class="fal fa-edit"></i> </button>\
574 <button onclick="deleteRow('${row.Id}',${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="删除记录" ><i class="fal fa-times"></i> </button>\
575 </div>`;
576 } else {
577 return `<button class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" disabled title="查看明细" ><i class="fal fa-edit"></i> </button>`;
578 }
579 }
580 }
581 ]],
582 columns: [[
583
584 { /*名称*/
585 field: 'Name',
586 title: '<span class="required">@Html.DisplayNameFor(model => model.Name)</span>',
587 width: 200,
588 hidden: false,
589 editor: {
590 type: 'textbox',
591 options: { prompt: '@Html.DescriptionFor(model => model.Name)', required: true, validType: 'length[0,50]' }
592 },
593 sortable: true,
594 resizable: true
595 },
596 { /*组织代码*/
597 field: 'Code',
598 title: '<span class="required">@Html.DisplayNameFor(model => model.Code)</span>',
599 width: 120,
600 hidden: false,
601 editor: {
602 type: 'textbox',
603 options: { prompt: '@Html.DescriptionFor(model => model.Code)', required: true, validType: 'length[0,12]' }
604 },
605 sortable: true,
606 resizable: true
607 },
608 { /*地址*/
609 field: 'Address',
610 title: '@Html.DisplayNameFor(model => model.Address)',
611 width: 200,
612 hidden: false,
613 editor: {
614 type: 'textbox',
615 options: { prompt: '@Html.DescriptionFor(model => model.Address)', required: false, validType: 'length[0,50]' }
616 },
617 sortable: true,
618 resizable: true
619 },
620 { /*联系人*/
621 field: 'Contect',
622 title: '@Html.DisplayNameFor(model => model.Contect)',
623 width: 120,
624 hidden: false,
625 editor: {
626 type: 'textbox',
627 options: { prompt: '@Html.DescriptionFor(model => model.Contect)', required: false, validType: 'length[0,12]' }
628 },
629 sortable: true,
630 resizable: true
631 },
632 { /*联系电话*/
633 field: 'PhoneNumber',
634 title: '@Html.DisplayNameFor(model => model.PhoneNumber)',
635 width: 120,
636 hidden: false,
637 editor: {
638 type: 'textbox',
639 options: { prompt: '@Html.DescriptionFor(model => model.PhoneNumber)', required: false, validType: 'length[0,20]' }
640 },
641 sortable: true,
642 resizable: true
643 },
644 { /*注册日期*/
645 field: 'RegisterDate',
646 title: '<span class="required">@Html.DisplayNameFor(model => model.RegisterDate)</span>',
647 width: 140,
648 align: 'right',
649 hidden: false,
650 editor: {
651 type: 'datebox',
652 options: { prompt: '@Html.DescriptionFor(model => model.RegisterDate)', required: true }
653 },
654 formatter: dateformatter,
655 sortable: true,
656 resizable: true
657 },
658 ]]
659 }).datagrid('columnMoving')
660 .datagrid('resetColumns')
661 .datagrid('enableFilter', [
662 { /*Id*/
663 field: 'Id',
664 type: 'numberbox',
665 op: ['equal', 'notequal', 'less', 'lessorequal', 'greater', 'greaterorequal']
666 },
667 { /*注册日期*/
668 field: 'RegisterDate',
669 type: 'dateRange',
670 options: {
671 onChange: value => {
672 $dg.datagrid('addFilterRule', {
673 field: 'RegisterDate',
674 op: 'between',
675 value: value
676 });
677
678 $dg.datagrid('doFilter');
679 }
680 }
681 },
682 ])
683 .datagrid('load', '/Companies/GetData');
684 }
685 );
686
687 </script>
688 <script type="text/javascript">
689 //判断新增编辑状态
690 var MODELSTATE = 'Added';
691 var companyid = null;
692 function opencompanydetailwindow(data, state) {
693 MODELSTATE = state;
694 initcompanydetailview();
695 companyid = (data.Id || 0);
696 $("#companydetailwindow").window("open");
697 $('#company_form').form('reset');
698 $('#company_form').form('load', data);
699 }
700 //删除当前记录
701 function deletecompanyitem() {
702 $.messager.confirm('确认', '你确定要删除该记录?', result => {
703 if (result) {
704 const url = `/Companies/Delete/${companyid}`;
705 $.get(url).done(res => {
706 if (res.success) {
707 toastr.success("删除成功");
708 $("#companydetailwindow").window("close");
709 reload();
710 } else {
711 $.messager.alert("错误", res.err, "error");
712 }
713 });
714 }
715 });
716 }
717 //async 保存数据
718 async function savecompanyitem() {
719 const $companyform = $('#company_form');
720 if ($companyform.form('enableValidation').form('validate')) {
721 let company = $companyform.serializeJSON();
722 let url = '/Companies/Edit';
723 //判断是新增或是修改方法
724 if (MODELSTATE === 'Added') {
725 url = '/Companies/Create';
726 }
727 var token = $('input[name="__RequestVerificationToken"]', $companyform).val();
728 //$.messager.progress({ title: '请等待', msg: '正在保存数据...', interval: 200 });
729 $.ajax({
730 type: "POST",
731 url: url,
732 data: {
733 __RequestVerificationToken: token,
734 company: company
735 },
736 dataType: 'json',
737 contentType: 'application/x-www-form-urlencoded; charset=utf-8'
738 })
739 .done(response => {
740 //$.messager.progress('close');
741 if (response.success) {
742 hook = false;
743 $companyform.form('disableValidation');
744 $dg.datagrid('reload');
745 $('#companydetailwindow').window("close");
746 toastr.success("保存成功");
747 } else {
748 $.messager.alert("错误", response.err, "error");
749 }
750 })
751 .fail((jqXHR, textStatus, errorThrown) => {
752 //$.messager.progress('close');
753 $.messager.alert('异常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');
754 });
755 }
756 }
757 //关闭窗口
758 function closecompanydetailwindow() {
759 $('#companydetailwindow').window('close');
760 }
761
762 //判断是否有没有保存的记录
763 function companyhasmodified() {
764 return hook;
765 }
766
767
768 function initcompanydetailview() {
769 //判断是否显示功能按钮
770 if (MODELSTATE === 'Added') {
771 $('#deleteitem-btn-group').hide();
772 } else {
773 $('#deleteitem-btn-group').show();
774 }
775
776 //回车光标移动到下个输入控件
777 //日期类型 注册日期
778 $('#RegisterDate').datebox('textbox').bind('keydown', function (e) {
779 if (e.keyCode == 13) {
780 $(e.target).emulateTab();
781 }
782 });
783 }
784 </script>
785 }
上面View层的代码非常的复杂,但都是固定格式,可以用scaffold快速生成
打开 startup.cs 在 public void ConfigureServices(IServiceCollection services) 注册服务 services.AddScoped<IRepositoryX, RepositoryX>(); services.AddScoped<ICustomerService, CustomerService>();
EF Core Code-First 同步更新数据库 在 Visual Studio.Net Package Manager Controle 运行 PM>:add-migration create_Company PM>:update-database PM>:更新完成
CAP 分布式事务的解决方案及应用场景 nuget 安装组件 PM> Install-Package DotNetCore.CAP PM> Install-Package DotNetCore.CAP.RabbitMQ PM> Install-Package DotNetCore.CAP.SqlServer \
1 public void ConfigureServices(IServiceCollection services)
2 {
3 services.AddCap(x =>
4 {
5 x.UseEntityFramework<SmartDbContext>();
6 x.UseRabbitMQ("127.0.0.1");
7 x.UseDashboard();
8 x.FailedRetryCount = 5;
9 x.FailedThresholdCallback = failed =>
10 {
11 var logger = failed.ServiceProvider.GetService<ILogger<Startup>>();
12 logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times,
13 requiring manual troubleshooting. Message name: {failed.Message.GetName()}");
14 };
15 });
16 }