feat: 增加后台任务管理模块
This commit is contained in:
parent
ea2fe0f536
commit
e5bac32955
|
@ -12,8 +12,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.9" />
|
||||
<PackageReference Include="Longbow.Configuration" Version="2.2.3" />
|
||||
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.10" />
|
||||
<PackageReference Include="Longbow.Configuration" Version="2.2.4" />
|
||||
<PackageReference Include="Longbow.Tasks" Version="1.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.1.2" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.7.10" />
|
||||
|
|
|
@ -114,7 +114,7 @@ namespace Bootstrap.Admin.Controllers
|
|||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ActionResult Tasks() => View(new NavigatorBarModel(this));
|
||||
public ActionResult Tasks() => View(new TaskModel(this));
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
|
|
@ -1,24 +1,66 @@
|
|||
using Bootstrap.DataAccess;
|
||||
using Longbow.Tasks;
|
||||
using Longbow.Web.SignalR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bootstrap.Admin.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class TasksController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public IEnumerable<Task> Get()
|
||||
public IEnumerable<object> Get()
|
||||
{
|
||||
return TaskHelper.Retrieves();
|
||||
TaskServicesManager.GetOrAdd("测试任务", token => Task.Delay(1000), TriggerBuilder.WithCronExpression("*/5 * * * * *"));
|
||||
return TaskServicesManager.ToList().Select(s => new { s.Name, s.Enabled, s.Status, s.LastRuntime, s.CreatedTime, s.NextRuntime, Triggers = s.Triggers.Count, s.LastRunResult, TriggerExpression = s.Triggers.FirstOrDefault().ToString() });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public bool Post()
|
||||
{
|
||||
// UNDONE: 待完善
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="hub"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPut]
|
||||
public bool Put([FromQuery]string name, [FromServices]IHubContext<SignalRHub> hub)
|
||||
{
|
||||
var sche = TaskServicesManager.GetOrAdd(name);
|
||||
sche.Triggers[0].RegisterPulseCallback(async t =>
|
||||
{
|
||||
var success = t.Cancelled ? "Cancelled" : "Success";
|
||||
var result = $"{t.Scheduler.LastRuntime.Value.DateTime}: Trigger({t.GetType().Name}) Run({success}) NextRuntime: {t.NextRuntime.Value.DateTime} Elapsed: {t.LastRunElapsedTime.Seconds}s";
|
||||
await SignalRManager.SendTaskLog(hub.Clients.All, result);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
public bool Delete() => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bootstrap.Admin.Models
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class TaskModel : NavigatorBarModel
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="controller"></param>
|
||||
public TaskModel(ControllerBase controller) : base(controller)
|
||||
{
|
||||
Tasks = new string[] { "测试任务" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得 系统配置的所有任务
|
||||
/// </summary>
|
||||
public IEnumerable<string> Tasks { get; }
|
||||
}
|
||||
}
|
|
@ -16,7 +16,16 @@ namespace Bootstrap.Admin
|
|||
/// <param name="client"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static async System.Threading.Tasks.Task Send(IClientProxy client, MessageBody args) => await client.SendAsync("rev", args);
|
||||
public static System.Threading.Tasks.Task Send(IClientProxy client, MessageBody args) => client.SendAsync("rev", args);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static System.Threading.Tasks.Task SendTaskLog(IClientProxy client, string args) => client.SendAsync("taskRev", args);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
|
|
@ -63,6 +63,7 @@ namespace Bootstrap.Admin
|
|||
services.AddBootstrapAdminAuthentication();
|
||||
services.AddSwagger();
|
||||
services.AddButtonAuthorization(MenuHelper.AuthorizateButtons);
|
||||
services.AddTaskServices();
|
||||
services.AddMvc(options =>
|
||||
{
|
||||
options.Filters.Add<BootstrapAdminAuthorizeFilter>();
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</form>
|
||||
}
|
||||
@section body {
|
||||
@section cardbody {
|
||||
<div class="alert alert-danger" role="alert" asp-condition="@Model.IsDemo">
|
||||
<span>演示系统禁止修改系统使用字典配置项</span>
|
||||
</div>
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
@section gear {
|
||||
<a id="tb_assignRole" href="#" title="分配角色" asp-auth="assignRole"><i class="fa fa-sitemap"></i></a>
|
||||
}
|
||||
@section body {
|
||||
@section cardbody {
|
||||
<div class="alert alert-danger" role="alert" asp-condition="@Model.IsDemo">
|
||||
<span>演示系统禁止修改系统菜单,可修改外部菜单</span>
|
||||
</div>
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<a id="tb_assignMenu" href="#" title="分配菜单" asp-auth="assignMenu"><i class="fa fa-dashboard"></i></a>
|
||||
<a id="tb_assignApp" href="#" title="分配应用" asp-auth="assignApp"><i class="fa fa-cubes"></i></a>
|
||||
}
|
||||
@section body {
|
||||
@section cardbody {
|
||||
<div class="alert alert-danger" role="alert" asp-condition="@Model.IsDemo">
|
||||
<span>演示系统禁止修改内置角色(Administrators、Default)</span>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,99 @@
|
|||
@model NavigatorBarModel
|
||||
@model TaskModel
|
||||
@{
|
||||
ViewBag.Title = "任务管理";
|
||||
Layout = "_Default";
|
||||
}
|
||||
@section css {
|
||||
<link href="~/lib/longbow-select/longbow-select.css" rel="stylesheet" />
|
||||
<link href="~/css/tasks.css" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
@section javascript {
|
||||
<script src="~/lib/longbow-select/longbow-select.js"></script>
|
||||
<script src="~/js/tasks.js" asp-append-version="true"></script>
|
||||
}
|
||||
<div class="card">
|
||||
<div class="card-header">任务消息<span class="pull-right"><a id="refreshTask" href="javascript:;" class="fa fa-refresh"></a></span></div>
|
||||
<div class="card-body">
|
||||
<div class="tasks-widget">
|
||||
<ul id="list-task" class="task-list ui-sortable"></ul>
|
||||
@section tableButtons {
|
||||
<button class='info btn btn-sm btn-info' asp-auth="info"><i class='fa fa-info'></i><span>日志</span></button>
|
||||
}
|
||||
@section cardbody {
|
||||
<div class="alert alert-danger" role="alert" asp-condition="@Model.IsDemo">
|
||||
<span>演示系统禁止修改定时后台任务</span>
|
||||
</div>
|
||||
}
|
||||
@section query {
|
||||
<div>
|
||||
<div class="tip">
|
||||
<p class="font-weight-bold">后台任务说明:</p>
|
||||
<p>1. 默认任务 (立即执行,仅执行一次)</p>
|
||||
<p>
|
||||
<code>
|
||||
TaskServicesManager.GetOrAdd("简单任务", token => Task.Delay(1000));
|
||||
</code>
|
||||
</p>
|
||||
<p>2. 周期性任务 (1 分钟后间隔 5 秒执行2次任务)</p>
|
||||
<p>
|
||||
<code>
|
||||
var trigger = TriggerBuilder.Default.WithInterval(TimeSpan.FromSeconds(5)).WithRepeatCount(2).WithStartTime(DateTimeOffset.Now.AddMinutes(1)).Build();
|
||||
TaskServicesManager.GetOrAdd("测试任务", token => Task.Delay(1000), trigger);
|
||||
</code>
|
||||
</p>
|
||||
<p>3. Cron表达式任务 (间隔 5 秒循环执行任务)</p>
|
||||
<p>
|
||||
<code>
|
||||
TaskServicesManager.GetOrAdd("Cron表达式任务", token => Task.Delay(1000), TriggerBuilder.WithCronExpression("*/5 * * * * *"));
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@section modal {
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="myModalLabel">任务编辑窗口</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline">
|
||||
<div class="row">
|
||||
<input type="hidden" id="taskID" />
|
||||
<div class="form-group col-12">
|
||||
<label class="control-label" for="taskName">任务名称</label>
|
||||
<input type="text" class="form-control flex-sm-fill" id="taskName" placeholder="不可为空,50字以内" maxlength="50" data-valid="true" />
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
<label class="control-label" for="taskCron">Cron表达式</label>
|
||||
<input type="text" class="form-control flex-sm-fill" id="taskCron" placeholder="不可为空,2000字以内" maxlength="2000" data-valid="true" />
|
||||
</div>
|
||||
<div class="form-group col-12">
|
||||
<label class="control-label" for="taskContent">内置任务</label>
|
||||
<select id="taskList" data-toggle="lgbSelect">
|
||||
@foreach (var name in Model.Tasks)
|
||||
{
|
||||
<option value="@name">@name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@section customModal {
|
||||
<div class="modal fade" id="dialogLog" tabindex="-1" role="dialog" data-backdrop="static" aria-labelledby="taskModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="taskModalLabel">任务日志窗口</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="background-color: #174482; color: #fff;">
|
||||
<div id="taskMsg" class="form-group" style="height: 500px; overflow: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
<i class="fa fa-times"></i>
|
||||
<span>关闭</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
|
@ -38,7 +38,7 @@
|
|||
@section tableButtons {
|
||||
<button class="reset btn btn-warning" asp-auth="resetPassword"><i class="fa fa-remove"></i><span>重置</span></button>
|
||||
}
|
||||
@section body {
|
||||
@section cardbody {
|
||||
<div class="alert alert-danger" role="alert" asp-condition="@Model.IsDemo">
|
||||
<span>演示系统禁止修改Admin账户</span>
|
||||
</div>
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
查询结果
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@await RenderSectionAsync("body", false)
|
||||
@await RenderSectionAsync("cardbody", false)
|
||||
<table></table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -90,4 +90,5 @@
|
|||
<button class='del btn btn-sm btn-danger' asp-auth="del"><i class='fa fa-remove'></i><span>删除</span></button>
|
||||
@RenderSection("tableButtons", false)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@await RenderSectionAsync("body", false)
|
|
@ -536,3 +536,11 @@ input.pending {
|
|||
.alert-danger {
|
||||
border-left: solid 4px #f78792;
|
||||
}
|
||||
|
||||
.tip {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #ecf8ff;
|
||||
border-radius: 4px;
|
||||
border-left: 5px solid #50bfff;
|
||||
margin-bottom: 1rem;
|
||||
}
|
|
@ -1,21 +1,82 @@
|
|||
$(function () {
|
||||
var htmlTask = '<li class="list-primary"><i class="fa fa-ellipsis-v"></i><div class="task-title notifi"><span class="task-title-sp">{0}</span><span class="task-value">{1}</span><span class="task-time">{2}</span><div class="pull-right hidden-phone"><button class="btn btn-success btn-xs fa fa-check" data-id="{3}" data-result="1"></button><button class="btn btn-danger btn-xs fa fa-remove" data-id="{3}" data-result="0" data-placement="left" data-original-title="拒绝授权"></button></div></div></li>';
|
||||
|
||||
$('#refreshTask').on('click', function () {
|
||||
var that = $(this);
|
||||
that.toggleClass('fa-spin');
|
||||
$.bc({
|
||||
url: Tasks.url,
|
||||
autoFooter: true,
|
||||
callback: function (result) {
|
||||
if (result) {
|
||||
var content = result.map(function (task) {
|
||||
return $.format(htmlTask, task.TaskName, task.UserName, task.AssignTime, task.Id);
|
||||
}).join('');
|
||||
$('#list-task').html(content);
|
||||
}
|
||||
that.toggleClass('fa-spin');
|
||||
var $taskMsg = $('#taskMsg');
|
||||
var $taskLogModelTitle = $('#taskModalLabel');
|
||||
var stateFormatter = function (value) {
|
||||
var template = "<button class='btn btn-sm btn-{0}'><i class='fa fa-{1}'></i><span>{2}<span></button>";
|
||||
var content = "";
|
||||
if (value === "0") {
|
||||
content = $.format(template, 'info', 'fa', '未开始');
|
||||
}
|
||||
else if (value === "1") {
|
||||
content = $.format(template, 'success', 'play-circle', '运行中');
|
||||
}
|
||||
else if (value === "2") {
|
||||
content = $.format(template, 'primary', 'stop-circle', '已停止');
|
||||
}
|
||||
else if (value === "3") {
|
||||
content = $.format(template, 'danger', 'times-circle', '已禁用');
|
||||
}
|
||||
return content;
|
||||
};
|
||||
var enabledFormatter = function (value) {
|
||||
var template = "<i class='fa fa-toggle-{0}'></i>";
|
||||
return $.format(template, value ? 'on' : 'off');
|
||||
};
|
||||
$('.card-body table').lgbTable({
|
||||
url: Tasks.url,
|
||||
dataBinder: {
|
||||
map: {
|
||||
Id: "#taskID",
|
||||
Name: "#taskName"
|
||||
}
|
||||
});
|
||||
}).trigger('click');
|
||||
},
|
||||
smartTable: {
|
||||
sortName: 'CreateTime',
|
||||
sortOrder: 'desc',
|
||||
queryParams: function (params) { return $.extend(params, { operateType: $("#txt_operate_type").val(), OperateTimeStart: $("#txt_operate_start").val(), OperateTimeEnd: $("#txt_operate_end").val() }); },
|
||||
columns: [
|
||||
{ title: "名称", field: "Name", sortable: true },
|
||||
{ title: "创建时间", field: "CreatedTime", sortable: true },
|
||||
{ title: "上次执行时间", field: "LastRuntime", sortable: true },
|
||||
{ title: "下次执行时间", field: "NextRuntime", sortable: true },
|
||||
{ title: "触发条件", field: "TriggerExpression", sortable: false },
|
||||
{ title: "是否启用", field: "Enabled", sortable: true, formatter: enabledFormatter },
|
||||
{ title: "状态", field: "Status", sortable: true, align: 'center', width: 106, formatter: stateFormatter }
|
||||
],
|
||||
editButtons: {
|
||||
events: {
|
||||
'click .info': function (e, value, row, index) {
|
||||
$taskLogModelTitle.html(row.Name + ' - 任务日志窗口(最新50条)');
|
||||
$.bc({
|
||||
url: 'api/Tasks?name=' + row.Name,
|
||||
method: 'put'
|
||||
});
|
||||
$('#dialogLog').modal('show').on('hide.bs.modal', function () {
|
||||
// close hub
|
||||
if ($taskMsg.hub) $taskMsg.hub.stop();
|
||||
$taskMsg.html('');
|
||||
});
|
||||
|
||||
var lastMsg = "";
|
||||
// open hub
|
||||
$taskMsg.notifi({
|
||||
url: 'NotiHub',
|
||||
method: 'taskRev',
|
||||
callback: function (result) {
|
||||
if (lastMsg === result) return;
|
||||
lastMsg = result;
|
||||
while (this.children().length > 50) {
|
||||
this.children().first().remove();
|
||||
}
|
||||
this.append('<div>' + result + '</div>');
|
||||
},
|
||||
onclose: function (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
.form-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-select .dropdown-menu {
|
||||
margin-top: 14px;
|
||||
max-height: 274px;
|
||||
overflow-x: hidden;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.form-select .dropdown-menu-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 0 6px 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent rgba(0,0,0,.15);
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
z-index: 1001;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.form-select .dropdown-menu, .form-select .dropdown-menu-arrow {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
|
||||
.form-select .dropdown-menu-arrow:after {
|
||||
content: " ";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 0 6px 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #fff;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: -6px;
|
||||
}
|
||||
|
||||
.form-select .form-select-input[readonly] {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-select.open .dropdown-menu, .form-select.open .dropdown-menu-arrow {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-select.open .form-select-append i {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
.form-select.is-disabled .form-select-input {
|
||||
background-color: #f5f7fa;
|
||||
border-color: #e4e7ed;
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-select-input {
|
||||
color: #606266;
|
||||
outline: none;
|
||||
padding-right: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select-input:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
|
||||
.form-select-input.primary:hover {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
|
||||
border-color: #66afe9;
|
||||
}
|
||||
|
||||
.form-select-input.info:hover {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(23, 162, 184, 0.5);
|
||||
border-color: #17a2b8;
|
||||
}
|
||||
|
||||
.form-select-input.success:hover {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(40, 167, 69, 0.5);
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.form-select-input.warning:hover {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(255, 193, 7, 0.5);
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.form-select-input.danger:hover {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(220, 53, 69, 0.5);
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
|
||||
.form-select-input:focus {
|
||||
border-color: #409eff;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
|
||||
}
|
||||
|
||||
.form-select-input.primary:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
|
||||
border-color: #66afe9;
|
||||
}
|
||||
|
||||
.form-select-input.info:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(23, 162, 184, 0.5);
|
||||
border-color: #17a2b8;
|
||||
}
|
||||
|
||||
.form-select-input.success:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(40, 167, 69, 0.5);
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.form-select-input.warning:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(255, 193, 7, 0.5);
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.form-select-input.danger:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(220, 53, 69, 0.5);
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
|
||||
.form-select-input.is-valid, .form-select-input.is-invalid {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.form-select-append {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
color: #c0c4cc;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-select-append i {
|
||||
transition: all .3s;
|
||||
transform: rotate(180deg);
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
var lgbSelect = function (element, options) {
|
||||
this.$element = $(element);
|
||||
this.options = $.extend({}, lgbSelect.DEFAULTS, options);
|
||||
this.init();
|
||||
};
|
||||
|
||||
lgbSelect.VERSION = '1.0';
|
||||
lgbSelect.Author = 'argo@163.com';
|
||||
lgbSelect.DataKey = "lgb.select";
|
||||
lgbSelect.Template = '<div class="form-select">';
|
||||
lgbSelect.Template += '<input type="text" readonly="readonly" class="form-control form-select-input" />';
|
||||
lgbSelect.Template += '<span class="form-select-append">';
|
||||
lgbSelect.Template += ' <i class="fa fa-angle-up"></i>';
|
||||
lgbSelect.Template += '</span>';
|
||||
lgbSelect.Template += '<div class="dropdown-menu-arrow"></div>';
|
||||
lgbSelect.Template += '<div class="dropdown-menu"></div>';
|
||||
lgbSelect.Template += '</div>';
|
||||
lgbSelect.DEFAULTS = {
|
||||
placeholder: "请选择 ...",
|
||||
borderClass: null
|
||||
};
|
||||
lgbSelect.AllowMethods = /disabled|enable|val|reset/;
|
||||
|
||||
function Plugin(option) {
|
||||
var params = $.makeArray(arguments).slice(1);
|
||||
return this.each(function () {
|
||||
var $this = $(this);
|
||||
var data = $this.data(lgbSelect.DataKey);
|
||||
var options = typeof option === 'object' && option;
|
||||
|
||||
if (!data) $this.data(lgbSelect.DataKey, data = new lgbSelect(this, options));
|
||||
if (!lgbSelect.AllowMethods.test(option)) return;
|
||||
if (typeof option === 'string') {
|
||||
data[option].apply(data, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.fn.lgbSelect = Plugin;
|
||||
$.fn.lgbSelect.Constructor = lgbSelect;
|
||||
|
||||
var _proto = lgbSelect.prototype;
|
||||
_proto.init = function () {
|
||||
var getUID = function (prefix) {
|
||||
if (!prefix) prefix = 'lgb';
|
||||
do prefix += ~~(Math.random() * 1000000);
|
||||
while (document.getElementById(prefix));
|
||||
return prefix;
|
||||
};
|
||||
|
||||
var that = this;
|
||||
this.$element.addClass('d-none');
|
||||
this.$ctl = $(lgbSelect.Template).insertBefore(this.$element);
|
||||
this.$input = this.$ctl.find('.form-select-input');
|
||||
this.$menus = this.$ctl.find('.dropdown-menu');
|
||||
|
||||
// init for
|
||||
var $for = this.$element.parent().find('[for="' + this.$element.attr('id') + '"]');
|
||||
if ($for.length > 0) {
|
||||
var id = getUID();
|
||||
this.$input.attr('id', id);
|
||||
$for.attr('for', id);
|
||||
}
|
||||
|
||||
if (this.options.borderClass) {
|
||||
this.$input.addClass(this.options.borderClass);
|
||||
}
|
||||
this.$input.attr('placeholder', this.options.placeholder);
|
||||
|
||||
// init dropdown-menu
|
||||
var data = this.$element.find('option').map(function () {
|
||||
return { value: this.value, text: this.text, selected: this.selected }
|
||||
});
|
||||
this.reset(data);
|
||||
|
||||
// bind attribute
|
||||
["data-valid", "data-required-msg"].forEach(function (v, index) {
|
||||
if (that.$element.attr(v) !== undefined) {
|
||||
that.$input.attr(v, that.$element.attr(v));
|
||||
}
|
||||
});
|
||||
|
||||
// replace element select -> input hidden
|
||||
var eid = this.$element.attr('id');
|
||||
this.$element.remove();
|
||||
this.$element = $('<input type="hidden" data-toggle="lgbSelect" />').attr('id', eid).val(that.val()).insertAfter(this.$ctl);
|
||||
this.$element.data(lgbSelect.DataKey, this);
|
||||
|
||||
// bind event
|
||||
this.$ctl.on('click', '.form-select-input', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
that.$ctl.toggleClass('open');
|
||||
// calc width
|
||||
that.$ctl.find('.dropdown-menu').outerWidth(that.$ctl.outerWidth());
|
||||
});
|
||||
|
||||
this.$ctl.on('click', 'a.dropdown-item', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $this = $(this);
|
||||
$this.parent().children().removeClass('active');
|
||||
that.val($this.attr('data-val'), true);
|
||||
});
|
||||
|
||||
$(document).on('click', function (e) {
|
||||
if (that.$input[0] !== e.target)
|
||||
that.closeMenu();
|
||||
});
|
||||
};
|
||||
|
||||
_proto.closeMenu = function () {
|
||||
this.$ctl.removeClass('open');
|
||||
};
|
||||
|
||||
_proto.disabled = function () {
|
||||
this.$ctl.addClass('is-disabled');
|
||||
this.$input.attr('disabled', 'disabled');
|
||||
};
|
||||
|
||||
_proto.enable = function () {
|
||||
this.$ctl.removeClass('is-disabled');
|
||||
this.$input.removeAttr('disabled');
|
||||
};
|
||||
|
||||
_proto.reset = function (value) {
|
||||
var that = this;
|
||||
// keep old value
|
||||
var oldValue = this.$element.val();
|
||||
this.val('');
|
||||
this.$menus.html('');
|
||||
$.each(value, function () {
|
||||
var $item = $('<a class="dropdown-item" href="#" data-val="' + this.value + '">' + this.text + '</a>');
|
||||
that.$menus.append($item);
|
||||
if (this.selected === true || this.value.toString() === oldValue) {
|
||||
that.val(this.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_proto.val = function (value, valid) {
|
||||
if (value !== undefined) {
|
||||
var text = this.$menus.find('a.dropdown-item[data-val="' + value + '"]').text();
|
||||
this.$input.val(text);
|
||||
this.$element.val(value).attr('data-text', text);
|
||||
this.$menus.find('.dropdown-item').removeClass('active');
|
||||
this.$menus.find('.dropdown-item[data-val="' + value + '"]').addClass('active');
|
||||
|
||||
// trigger changed.lgbselect
|
||||
this.$element.trigger('changed.lgbSelect');
|
||||
|
||||
// trigger lgbValidate
|
||||
if (valid && this.$input.attr('data-valid') === 'true') this.$input.trigger('input.lgb.validate');
|
||||
}
|
||||
else {
|
||||
return this.$element.val();
|
||||
}
|
||||
};
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="lgbSelect"]').lgbSelect();
|
||||
});
|
||||
})(jQuery);
|
|
@ -444,8 +444,12 @@
|
|||
if ($.isFunction(op.callback)) op.callback.apply(that, arguments);
|
||||
return console.error(err.toString());
|
||||
}).then(function () {
|
||||
// 连接成功
|
||||
// invoke 为 调用服务端方法
|
||||
// invoke: function (connection) { return connection.invoke('RetrieveDashboard'); }
|
||||
if (op.invoke) op.invoke(connection).then(function (result) { console.log(result); }).catch(function (err) { console.error(err.toString()); });
|
||||
});
|
||||
this.hub = connection;
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.2.5" />
|
||||
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.2.6" />
|
||||
<PackageReference Include="Longbow.Cache" Version="2.2.12" />
|
||||
<PackageReference Include="Longbow.Data" Version="2.3.2" />
|
||||
<PackageReference Include="Longbow.Web" Version="2.2.14" />
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.9" />
|
||||
<PackageReference Include="Longbow.Configuration" Version="2.2.3" />
|
||||
<PackageReference Include="Longbow.Logging" Version="2.2.8" />
|
||||
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.10" />
|
||||
<PackageReference Include="Longbow.Configuration" Version="2.2.4" />
|
||||
<PackageReference Include="Longbow.Logging" Version="2.2.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.2.5" />
|
||||
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.2.6" />
|
||||
<PackageReference Include="Longbow.Data" Version="2.3.2" />
|
||||
<PackageReference Include="Longbow.Logging" Version="2.2.8" />
|
||||
<PackageReference Include="Longbow.Logging" Version="2.2.9" />
|
||||
<PackageReference Include="Longbow.Security.Cryptography" Version="1.3.0" />
|
||||
<PackageReference Include="Longbow.Web" Version="2.2.14" />
|
||||
<PackageReference Include="Longbow.Cache" Version="2.2.12" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.28307.572
|
||||
VisualStudioVersion = 15.0.28307.705
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SQLServer", "SQLServer", "{87319AF5-7C40-4362-B67C-35F9DD737DB4}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
public class TaskTest
|
||||
{
|
||||
private ITestOutputHelper _helper;
|
||||
|
||||
public TaskTest(ITestOutputHelper helper) => _helper = helper;
|
||||
|
||||
[Fact]
|
||||
public async void Task_Ok()
|
||||
{
|
||||
var cts = new CancellationTokenSource(3000);
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
// await Task.Delay(3000, cts.Token);
|
||||
|
||||
await TestAsync(cts.Token);
|
||||
}
|
||||
catch (Exception) { }
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
}
|
||||
_helper.WriteLine(sw.ElapsedMilliseconds.ToString());
|
||||
|
||||
//var waiter = new CancellationTokenSource(1000);
|
||||
//var task = TestAsync(waiter.Token);
|
||||
//task.Wait(waiter.Token);
|
||||
////try
|
||||
////{
|
||||
//// task.Wait(2000, waiter.Token);
|
||||
////}
|
||||
////catch (Exception) { }
|
||||
//sw.Stop();
|
||||
}
|
||||
|
||||
private Task TestAsync(CancellationToken token) => Task.Delay(2000, token);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue