在 ASP.NET Core (.NET 6) 中构建可视化医生预约排班系统





5.00/5 (16投票s)
使用 ASP.NET Core、Entity Framework、DayPilot 和原生 JavaScript 构建医生预约排班 Web 应用程序。
预约排班用户界面
患者用户界面
医生预约排班项目包含一个专门的患者用户界面,患者可以在其中查看空闲预约时段。
患者看到的是一个简化的日历,带有周视图。在左侧,应用程序显示一个日期选择器日历,显示三个月。这有助于在周之间快速导航。有空闲预约时段的日期使用粗体字体。这样,患者可以快速查看第一个可用日期,并可以选择他们的预约日期和时间。
患者用户界面的主要目的是让他们选择一个空闲的预约时段。其他患者的预约以及过去的预约时段都已隐藏。这些时段是只读的,不能移动。一旦患者选择了一个时段,颜色就会变为橙色,表示“等待”状态(有关预约时段状态的更多信息,请参阅下文)。预约请求需要得到确认。
医生用户界面
医生使用单独的区域管理预约 - 创建、移动和编辑预约时段。
医生用户界面更高级 - 它显示所有预约时段,医生对它们拥有完全控制权。他们可以编辑预约详情,确认患者发送的请求,使用拖放将时段移动到不同的时间,创建自定义时段并删除它们。
所有预约时段都需要提前定义 - 我们将在下一节中更详细地讨论逻辑。
基本组件
- ASP.NET Core (.NET 6)、Entity Framework 和 SQL Server (LocalDB) 用于 REST API。应用程序用 C# 编写。
- DayPilot JavaScript 调度库(开源)、HTML5 和原生 JavaScript 用于创建用户界面。
虽然您可以在 Angular、React 和 Vue 中使用 DayPilot 日历组件,但这次我们将使用不带任何框架的简单 JavaScript。
有关使用 DayPilot 的 JavaScript 日历组件的介绍,请参阅 HTML5/JavaScript 事件日历 [code.daypilot.org] 教程。
工作原理
处理带有公共接口的医生预约有两种主要方法。
- 您可以定义显示可用时间的工作时间。患者可以在这些工作时间内创建新预约。这样,应用程序需要应用额外的规则,例如预约开始时间和持续时间。当患者请求会议时,会创建预约数据库记录。
- 您可以提前定义独立时段,患者只能选择现有时段之一。不需要额外的规则,因为医生对时段拥有完全控制权。在生成时段时会创建数据库记录。当患者请求会议时,他们只会更改时段状态。
在此项目中,我们将使用方法 #2,即预定义时段。
此工作流程要求提前定义时段。此应用程序使用半自动系统,可让您为特定范围生成时段(而不是逐个添加)。
ASP.NET Core 中的日、周和月日历视图
主要调度界面使用 DayPilot 的 JavaScript 日历组件创建。
它是您在 Google 日历或 Outlook 中熟悉的传统日/周/月视图。
在此应用程序中,我们扩展了日历应用程序的功能,增加了调度功能。
- 事件/预约具有状态,该状态决定了时段的颜色。
- 时段管理部分,可让您批量创建或删除时段。
医生部分的前端部分在 Doctor.cshml
ASP.NET Core 视图中定义。
该视图加载 DayPilot 调度 JavaScript 库
<script src="~/lib/daypilot/daypilot-all.min.js"></script>
并包含日历视图(日、周和月)的占位符。日历组件将在此处显示。切换视图时,我们将根据需要隐藏和显示组件。
<div id="day"></div>
<div id="week"></div>
<div id="month"></div>
现在我们需要初始化日历组件。每个组件都有自己的实例——日视图和周视图使用DayPilot.Calendar类,月视图使用DayPilot.Month类。
您会看到配置非常简单 - 我们依赖默认值。但是,必须添加定义行为的事件处理程序。
日视图
const day = new DayPilot.Calendar("day", {
viewType: "Day",
visible: false,
eventDeleteHandling: "Update",
onTimeRangeSelected: (args) => {
app.createAppointmentSlot(args);
},
onEventMoved: (args) => {
app.moveAppointmentSlot(args);
},
onEventResized: (args) => {
app.moveAppointmentSlot(args);
},
onEventDeleted: (args) => {
app.deleteAppointmentSlot(args);
},
onBeforeEventRender: (args) => {
app.renderAppointmentSlot(args);
},
onEventClick: (args) => {
app.editAppointmentSlot(args);
}
});
day.init();
周视图
const week = new DayPilot.Calendar("week", {
viewType: "Week",
eventDeleteHandling: "Update",
onTimeRangeSelected: (args) => {
app.createAppointmentSlot(args);
},
onEventMoved: (args) => {
app.moveAppointmentSlot(args);
},
onEventResized: (args) => {
app.moveAppointmentSlot(args);
},
onEventDeleted: (args) => {
app.deleteAppointmentSlot(args);
},
onBeforeEventRender: (args) => {
app.renderAppointmentSlot(args);
},
onEventClick: (args) => {
app.editAppointmentSlot(args);
}
});
week.init();
月视图
const month = new DayPilot.Month("month", {
visible: false,
eventDeleteHandling: "Update",
eventMoveHandling: "Disabled",
eventResizeHandling: "Disabled",
cellHeight: 150,
onCellHeaderClick: args => {
nav.selectMode = "Day";
nav.select(args.start);
},
onEventDelete: args => {
app.deleteAppointmentSlot(args);
},
onBeforeEventRender: args => {
app.renderAppointmentSlot(args);
// customize content
const locale = DayPilot.Locale.find(month.locale);
const start = new DayPilot.Date(args.data.start).toString(locale.timePattern);
const name = DayPilot.Util.escapeHtml(args.data.patientName || "");
args.data.html = `<span class='month-time'>${start}</span> ${name}`;
},
onTimeRangeSelected: async (args) => {
const params = {
start: args.start.toString(),
end: args.end.toString(),
weekends: true
};
args.control.clearSelection();
const {data} = await DayPilot.Http.post("/api/appointments/create", params);
app.loadEvents();
},
onEventClick: (args) => {
app.editAppointmentSlot(args);
}
});
month.init();
除了日历视图,我们还添加了一个日期选择器,让医生可以轻松更改日期。
占位符是一个空的 <div>
元素
<div id="nav"></div>
我们需要使用 DayPilot.Navigator 类初始化日期选择器。
const nav = new DayPilot.Navigator("nav", {
selectMode: "Week",
showMonths: 3,
skipMonths: 3,
onTimeRangeSelected: (args) => {
app.loadEvents(args.day);
}
});
nav.init();
app.loadEvents()
方法很重要,因为它从服务器加载预约时段。它检查当前视图类型(使用 nav.selectMode
)并加载数据。
const app = {
async loadEvents(date) {
const start = nav.visibleStart();
const end = nav.visibleEnd();
const {data} = await DayPilot.Http.get(`/api/appointments?start=${start}&end=${end}`);
const options = {
visible: true,
events: data
};
if (date) {
options.startDate = date;
}
day.hide();
week.hide();
month.hide();
const active = app.active();
active.update(options);
nav.update({
events: data
});
},
// ...
};
请注意,它加载日期选择器中可见的完整日期范围(三个月)的预约数据。这样,日期选择器可以突出显示有预约的日期。此数据将用于当前可见的日历组件(日历日期范围始终是日期选择器中可见的完整范围的子集)。
服务器端部分是一个标准的 ASP.NET Core API 控制器,由 Entity Framework 模型类生成。在某些情况下,当我们需要执行特定操作(除了基本的 create
、update
、delete
、select
操作)或需要额外参数时,会扩展控制器方法。
GetAppointments()
方法(GET /api/appointments
端点)如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;
using Project.Service;
namespace Project.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AppointmentsController : ControllerBase
{
private readonly DoctorDbContext _context;
public AppointmentsController(DoctorDbContext context)
{
_context = context;
}
// GET: api/Appointments
[HttpGet]
public async Task<ActionResult<IEnumerable<AppointmentSlot>>>
GetAppointments([FromQuery] DateTime start, [FromQuery] DateTime end)
{
return await _context.Appointments.Where(e => !((e.End <= start) ||
(e.Start >= end))).ToListAsync();
}
// ...
}
}
生成预约时段
在医生用户界面中的每个日历视图中,您可以使用“生成”按钮填充当前范围的预约时段。
该按钮会打开一个模态对话框,您可以在其中选择是否也要为周末生成预约时段。
在月视图中,您还可以使用拖放功能选择任意范围。
选择日期范围将为指定日期生成时段。
它将跳过与现有预约时段冲突的时间。
时段生成逻辑在服务器端实现。AppointmentController
类的 PostAppointmentSlots()
方法使用 Timeline
类辅助工具生成时段日期。我们在此处不详细介绍 Timeline
类,但您可以在项目源代码中查看其实现。
PostAppointmentSlots()
方法还会为日期范围选择现有时段,并在 SQL Server 数据库中创建新的时段记录之前检查重叠情况。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;
using Project.Service;
namespace Project.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AppointmentsController : ControllerBase
{
private readonly DoctorDbContext _context;
public AppointmentsController(DoctorDbContext context)
{
_context = context;
}
[HttpPost("create")]
public async Task<ActionResult<AppointmentSlot>>
PostAppointmentSlots(AppointmentSlotRange range)
{
var existing = await _context.Appointments.Where(e => !((e.End <= range.Start) ||
(e.Start >= range.End))).ToListAsync();
var slots = Timeline.GenerateSlots(range.Start, range.End, range.Weekends);
slots.ForEach(slot =>
{
var overlaps = existing.Any(e => !((e.End <= slot.Start) ||
(e.Start >= slot.End)));
if (overlaps)
{
return;
}
_context.Appointments.Add(slot);
});
await _context.SaveChangesAsync();
return NoContent();
}
public class AppointmentSlotRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public bool Weekends { get; set; }
}
// ...
}
}
预约时段状态
每个预约时段可以标记为“可用”、“等待”或“已确认”。
可用
等待确认
已确认
时段状态存储在 AppointmentSlot
类的 Status
属性中。API 控制器将其作为预约数据对象的 status 字段发送到客户端。
using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;
namespace Project.Models
{
public class AppointmentSlot
{
public int Id { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
public string? PatientName { set; get; }
public string? Text => PatientName;
[JsonPropertyName("patient")]
public string? PatientId { set; get; }
public string Status { get; set; } = "free";
}
}
DayPilot JavaScript 日历组件提供了一个 onBeforeEventRender 事件处理程序,允许您在渲染之前自定义预约外观。我们将使用它根据时段状态应用自定义颜色。
const week = new DayPilot.Calendar("week", {
viewType: "Week",
onBeforeEventRender: (args) => {
switch (args.data.status) {
case "free":
args.data.backColor = app.colors.blue;
args.data.barColor = app.colors.blueDarker;
args.data.borderColor = "darker";
args.data.fontColor = app.colors.text;
args.data.text = "Available";
break;
case "waiting":
args.data.backColor = app.colors.orange;
args.data.barColor = app.colors.orangeDarker;
args.data.borderColor = "darker";
args.data.fontColor = app.colors.text;
args.data.text = args.data.patientName;
break;
case "confirmed":
args.data.backColor = app.colors.green;
args.data.barColor = app.colors.greenDarker;
args.data.borderColor = "darker";
args.data.fontColor = app.colors.text;
args.data.text = args.data.patientName;
break;
}
},
// ...
});
如何运行 ASP.NET Core 项目
所有内容都已包含
- 服务器端依赖项 (Entity Framework) 由 NuGet 管理。Visual Studio 通常会在第一次构建时自动为您加载 NuGet 依赖项。
- 客户端依赖项 (DayPilot) 位于 wwwroot/lib 文件夹中。
- 数据库需要安装 SQL Server Express LocalDB 实例。如果需要,您可以修改 appsettings.json 文件中的数据库连接字符串。初始的 Entity Framework 迁移已包含(请参阅 Migrations 文件夹),但您需要在控制台中运行 Update-Database 以应用它们。
历史
- 2022 年 1 月 12 日:首次发布