试用前走查的修改:报错看得见、学生找得到作业、运行区和设计器的若干体验问题
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 运行区:编译错误、运行异常、控制台输出和保存的图片移到窗体下面,不再被实训台挤成 0 高;缩放和放大按钮移到桌面上方,不压窗体的关闭按钮;放大后也能停止程序;点停止后清掉窗体
- 运行异常:常见 .NET 异常配中文说明,显示出错的文件、行号和方法,点击跳到代码
- 运行中的下拉列表挂到页面最外层,能伸出窗体和分组框(小窗体也能选到 COM4、COM5)
- 学生登录后进"我的作业";任务面板从工具栏下面开始,不挡导航;记住上次做的实验,从数据库页面返回、恢复数据时默认用它
- 代码页和设计页左栏都占 3/5,文件标签放不下时换行;实验选择框加宽;关闭粘性滚动
- 设计器:双击生成的事件方法带缩进;双击工具箱放进选中控件所在的容器并错开位置;新控件比容器宽时按比例缩小;无边框窗体不画标题栏
- 中文界面:代码编辑器(查找替换等)、属性窗口的颜色选项
- SQL:DATEADD、DATEDIFF、DATEPART 的日期单位(day、hour)解析成 IdentifierLiteral 时报"不支持",补上
- 项目 8 的预置环境数据改成建库那天之前的 7 天;p6-t3 评分项改为"按日期查询环境数据"
- 没有 AI 建议时不显示"全部采纳 AI 建议"和相关说明;导出 CSV 只给纯数字的文字加制表符

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 08:49:42 +08:00
1 parent da623fa648
commit f6024dc113
30 files changed
+537 -255

No files matched your search

+2 -1
View File
@@ -802,9 +802,10 @@ internal sealed class Translator
}
}
/// <summary>DATEADD(day, ...) 的第一个参数是关键字,解析成列名</summary>
/// <summary>DATEADD(day, ...) 的第一个参数是关键字:ScriptDom 解析成 IdentifierLiteral(旧版本是列名)</summary>
private static string DatePartName(ScalarExpression expression) => expression switch
{
IdentifierLiteral i => i.Value,
ColumnReferenceExpression c => c.MultiPartIdentifier.Identifiers[^1].Value,
StringLiteral s => s.Value,
_ => throw SqlException.NotSupported("这种 datepart 写法"),
+46 -4
View File
@@ -134,12 +134,54 @@ public static class SimHost
public static void ReportException(Exception ex)
{
while (ex is TargetInvocationException { InnerException: not null } wrapper) ex = wrapper.InnerException!;
Send(new JsonObject
var type = ex.GetType().FullName;
// 仿真库自己抛的异常信息已经是中文;.NET 自带的是英文,常见的几种配上中文说明,原文放在括号里
var message = HasChinese(ex.Message) ? $"{ex.Message}({type})"
: ExplainException(ex) is { } explained ? $"{explained}({type}: {ex.Message})"
: $"{type}: {ex.Message}";
var frame = StudentFrame(ex);
var result = new JsonObject
{
["type"] = "exception",
["message"] = $"{ex.GetType().FullName}: {ex.Message}",
["detail"] = StudentStackTrace(ex),
});
["message"] = message,
["detail"] = frame is { } f ? $"出错位置:{f.File} 第 {f.Line} 行({f.Method} 方法)" : StudentStackTrace(ex),
};
if (frame is { } at)
{
result["file"] = at.File;
result["line"] = at.Line;
}
Send(result);
}
private static bool HasChinese(string text) => text.Any(c => c >= '\u4e00' && c <= '\u9fff');
private static string? ExplainException(Exception ex) => ex switch
{
FormatException => "文字转换成数字或日期时格式不对,比如文本框是空的,或者里面不是数字",
NullReferenceException => "用到的对象是 null:变量还没有赋值,或者对象、窗体还没有 new 出来",
IndexOutOfRangeException => "数组的下标超出了范围:下标从 0 开始,最大是长度减 1",
ArgumentOutOfRangeException => "下标或参数超出了范围,比如列表、字符串的下标太大,或者下拉框还没有选中",
DivideByZeroException => "整数除以了 0",
OverflowException => "数字太大或太小,超出了这个类型能表示的范围",
InvalidCastException => "类型转换失败,这个对象不能转换成要求的类型",
KeyNotFoundException => "字典里没有这个键",
FileNotFoundException => "找不到这个文件",
DirectoryNotFoundException => "找不到这个文件夹",
_ => null,
};
/// <summary>堆栈里离出错点最近的一行学生代码:文件名、行号、方法名</summary>
private static (string File, int Line, string Method)? StudentFrame(Exception ex)
{
foreach (var frame in new System.Diagnostics.StackTrace(ex, true).GetFrames())
{
var file = frame.GetFileName();
var line = frame.GetFileLineNumber();
if (file == null || line <= 0 || file.Contains("/runtime/Sim.")) continue;
return (Path.GetFileName(file), line, frame.GetMethod()?.Name ?? "");
}
return null;
}
/// <summary>只保留学生代码的堆栈行,去掉仿真库内部的调用</summary>
+1 -1
View File
@@ -1,7 +1,7 @@
# 评分规则:书上评价表的功能项。每项单独启动一次程序(使用 database.sql 建的初始数据库),按步骤操作,核对结果。
# 初始数据有 3 行:2024-01-16 09:00、2024-01-18 14:30、2024-01-23 08:15
# 格式说明见 server/src/grading/checks.ts
- title: 能实现按日期查询灾情数据
- title: 能实现按日期查询环境数据
steps:
- pick: { control: StartTime, value: '2024-01-15' }
- pick: { control: EndTime, value: '2024-01-20' }
@@ -1,5 +1,5 @@
-- 实验的初始数据库 WisdomCampus(教材"综合实训"附带的库):环境数据表 EnvironmentData、报警图片表 SecurityData、用户表 Users。
-- EnvironmentData 预置了 7 天的历史数据,"数据分析"的图表一打开就有内容;SecurityData 是空的,拍照或有人入侵时才有记录。
-- EnvironmentData 预置了今天之前 7 天的历史数据(按建库那天往前推),"数据分析"的图表一打开就有最近一周的内容;SecurityData 是空的,拍照或有人入侵时才有记录。
-- 学生可以在数据库页面"恢复实验初始数据";评分时每个检查项用一份新的。
CREATE DATABASE WisdomCampus
GO
@@ -10,45 +10,45 @@ CREATE TABLE EnvironmentData (Id int IDENTITY(1,1) PRIMARY KEY, Fire bit NOT NUL
CREATE TABLE SecurityData (Id int IDENTITY(1,1) PRIMARY KEY, EvenTime datetime NOT NULL, EvenImage image NOT NULL)
GO
INSERT INTO Users (UserName, Password) VALUES ('admin', '123')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 58, 258, '2026-10-14 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.1, 53, 475, '2026-10-14 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 761, '2026-10-14 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.5, 49, 733, '2026-10-14 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 54, 528, '2026-10-14 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 283, '2026-10-14 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.9, 59, 261, '2026-10-15 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 53, 499, '2026-10-15 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 764, '2026-10-15 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.5, 47, 778, '2026-10-15 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 503, '2026-10-15 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 56, 218, '2026-10-15 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 13.9, 63, 258, '2026-10-16 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 60, 477, '2026-10-16 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 741, '2026-10-16 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.5, 54, 796, '2026-10-16 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 488, '2026-10-16 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 59, 214, '2026-10-16 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 11.9, 67, 234, '2026-10-17 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 63, 544, '2026-10-17 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.3, 60, 804, '2026-10-17 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.5, 54, 801, '2026-10-17 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.8, 59, 524, '2026-10-17 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 58, 286, '2026-10-17 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.4, 59, 250, '2026-10-18 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 55, 533, '2026-10-18 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.3, 50, 749, '2026-10-18 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 24, 48, 766, '2026-10-18 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.8, 54, 534, '2026-10-18 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.1, 53, 241, '2026-10-18 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.4, 54, 253, '2026-10-19 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.1, 50, 474, '2026-10-19 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.3, 48, 778, '2026-10-19 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 26, 46, 783, '2026-10-19 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.3, 50, 520, '2026-10-19 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.6, 50, 213, '2026-10-19 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 62, 230, '2026-10-20 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.6, 52, 471, '2026-10-20 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 53, 751, '2026-10-20 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.5, 48, 747, '2026-10-20 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 527, '2026-10-20 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 220, '2026-10-20 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 58, 258, DATEADD(hour, 8, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.1, 53, 475, DATEADD(hour, 10, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 761, DATEADD(hour, 12, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.5, 49, 733, DATEADD(hour, 14, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 54, 528, DATEADD(hour, 16, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 283, DATEADD(hour, 18, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.9, 59, 261, DATEADD(hour, 8, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 53, 499, DATEADD(hour, 10, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 764, DATEADD(hour, 12, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.5, 47, 778, DATEADD(hour, 14, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 503, DATEADD(hour, 16, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 56, 218, DATEADD(hour, 18, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 13.9, 63, 258, DATEADD(hour, 8, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 60, 477, DATEADD(hour, 10, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 741, DATEADD(hour, 12, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.5, 54, 796, DATEADD(hour, 14, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 488, DATEADD(hour, 16, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 59, 214, DATEADD(hour, 18, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 11.9, 67, 234, DATEADD(hour, 8, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 63, 544, DATEADD(hour, 10, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.3, 60, 804, DATEADD(hour, 12, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.5, 54, 801, DATEADD(hour, 14, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.8, 59, 524, DATEADD(hour, 16, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 58, 286, DATEADD(hour, 18, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.4, 59, 250, DATEADD(hour, 8, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 55, 533, DATEADD(hour, 10, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.3, 50, 749, DATEADD(hour, 12, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 24, 48, 766, DATEADD(hour, 14, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.8, 54, 534, DATEADD(hour, 16, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.1, 53, 241, DATEADD(hour, 18, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.4, 54, 253, DATEADD(hour, 8, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.1, 50, 474, DATEADD(hour, 10, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.3, 48, 778, DATEADD(hour, 12, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 26, 46, 783, DATEADD(hour, 14, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.3, 50, 520, DATEADD(hour, 16, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.6, 50, 213, DATEADD(hour, 18, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 62, 230, DATEADD(hour, 8, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.6, 52, 471, DATEADD(hour, 10, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 53, 751, DATEADD(hour, 12, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.5, 48, 747, DATEADD(hour, 14, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 527, DATEADD(hour, 16, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 220, DATEADD(hour, 18, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
+43 -43
View File
@@ -1,5 +1,5 @@
-- 实验的初始数据库 WisdomCampus(教材"综合实训"附带的库):环境数据表 EnvironmentData、报警图片表 SecurityData、用户表 Users。
-- EnvironmentData 预置了 7 天的历史数据,"数据分析"的图表一打开就有内容;SecurityData 是空的,拍照或有人入侵时才有记录。
-- EnvironmentData 预置了今天之前 7 天的历史数据(按建库那天往前推),"数据分析"的图表一打开就有最近一周的内容;SecurityData 是空的,拍照或有人入侵时才有记录。
-- 学生可以在数据库页面"恢复实验初始数据";评分时每个检查项用一份新的。
CREATE DATABASE WisdomCampus
GO
@@ -10,45 +10,45 @@ CREATE TABLE EnvironmentData (Id int IDENTITY(1,1) PRIMARY KEY, Fire bit NOT NUL
CREATE TABLE SecurityData (Id int IDENTITY(1,1) PRIMARY KEY, EvenTime datetime NOT NULL, EvenImage image NOT NULL)
GO
INSERT INTO Users (UserName, Password) VALUES ('admin', '123')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 58, 258, '2026-10-14 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.1, 53, 475, '2026-10-14 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 761, '2026-10-14 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.5, 49, 733, '2026-10-14 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 54, 528, '2026-10-14 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 283, '2026-10-14 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.9, 59, 261, '2026-10-15 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 53, 499, '2026-10-15 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 764, '2026-10-15 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.5, 47, 778, '2026-10-15 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 503, '2026-10-15 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 56, 218, '2026-10-15 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 13.9, 63, 258, '2026-10-16 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 60, 477, '2026-10-16 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 741, '2026-10-16 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.5, 54, 796, '2026-10-16 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 488, '2026-10-16 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 59, 214, '2026-10-16 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 11.9, 67, 234, '2026-10-17 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 63, 544, '2026-10-17 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.3, 60, 804, '2026-10-17 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.5, 54, 801, '2026-10-17 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.8, 59, 524, '2026-10-17 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 58, 286, '2026-10-17 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.4, 59, 250, '2026-10-18 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 55, 533, '2026-10-18 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.3, 50, 749, '2026-10-18 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 24, 48, 766, '2026-10-18 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.8, 54, 534, '2026-10-18 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.1, 53, 241, '2026-10-18 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.4, 54, 253, '2026-10-19 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.1, 50, 474, '2026-10-19 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.3, 48, 778, '2026-10-19 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 26, 46, 783, '2026-10-19 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.3, 50, 520, '2026-10-19 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.6, 50, 213, '2026-10-19 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 62, 230, '2026-10-20 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.6, 52, 471, '2026-10-20 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 53, 751, '2026-10-20 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.5, 48, 747, '2026-10-20 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 527, '2026-10-20 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 220, '2026-10-20 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 58, 258, DATEADD(hour, 8, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.1, 53, 475, DATEADD(hour, 10, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 761, DATEADD(hour, 12, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.5, 49, 733, DATEADD(hour, 14, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 54, 528, DATEADD(hour, 16, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 283, DATEADD(hour, 18, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.9, 59, 261, DATEADD(hour, 8, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 53, 499, DATEADD(hour, 10, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 764, DATEADD(hour, 12, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.5, 47, 778, DATEADD(hour, 14, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 503, DATEADD(hour, 16, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 56, 218, DATEADD(hour, 18, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 13.9, 63, 258, DATEADD(hour, 8, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 60, 477, DATEADD(hour, 10, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 741, DATEADD(hour, 12, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.5, 54, 796, DATEADD(hour, 14, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 488, DATEADD(hour, 16, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 59, 214, DATEADD(hour, 18, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 11.9, 67, 234, DATEADD(hour, 8, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 63, 544, DATEADD(hour, 10, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.3, 60, 804, DATEADD(hour, 12, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.5, 54, 801, DATEADD(hour, 14, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.8, 59, 524, DATEADD(hour, 16, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 58, 286, DATEADD(hour, 18, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.4, 59, 250, DATEADD(hour, 8, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 55, 533, DATEADD(hour, 10, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.3, 50, 749, DATEADD(hour, 12, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 24, 48, 766, DATEADD(hour, 14, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.8, 54, 534, DATEADD(hour, 16, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.1, 53, 241, DATEADD(hour, 18, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.4, 54, 253, DATEADD(hour, 8, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.1, 50, 474, DATEADD(hour, 10, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.3, 48, 778, DATEADD(hour, 12, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 26, 46, 783, DATEADD(hour, 14, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.3, 50, 520, DATEADD(hour, 16, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.6, 50, 213, DATEADD(hour, 18, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 62, 230, DATEADD(hour, 8, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.6, 52, 471, DATEADD(hour, 10, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 53, 751, DATEADD(hour, 12, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.5, 48, 747, DATEADD(hour, 14, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 527, DATEADD(hour, 16, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 220, DATEADD(hour, 18, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
+43 -43
View File
@@ -1,5 +1,5 @@
-- 实验的初始数据库 WisdomCampus(教材"综合实训"附带的库):环境数据表 EnvironmentData、报警图片表 SecurityData、用户表 Users。
-- EnvironmentData 预置了 7 天的历史数据,"数据分析"的图表一打开就有内容;SecurityData 是空的,拍照或有人入侵时才有记录。
-- EnvironmentData 预置了今天之前 7 天的历史数据(按建库那天往前推),"数据分析"的图表一打开就有最近一周的内容;SecurityData 是空的,拍照或有人入侵时才有记录。
-- 学生可以在数据库页面"恢复实验初始数据";评分时每个检查项用一份新的。
CREATE DATABASE WisdomCampus
GO
@@ -10,45 +10,45 @@ CREATE TABLE EnvironmentData (Id int IDENTITY(1,1) PRIMARY KEY, Fire bit NOT NUL
CREATE TABLE SecurityData (Id int IDENTITY(1,1) PRIMARY KEY, EvenTime datetime NOT NULL, EvenImage image NOT NULL)
GO
INSERT INTO Users (UserName, Password) VALUES ('admin', '123')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 58, 258, '2026-10-14 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.1, 53, 475, '2026-10-14 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 761, '2026-10-14 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.5, 49, 733, '2026-10-14 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 54, 528, '2026-10-14 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 283, '2026-10-14 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.9, 59, 261, '2026-10-15 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 53, 499, '2026-10-15 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 764, '2026-10-15 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.5, 47, 778, '2026-10-15 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 503, '2026-10-15 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 56, 218, '2026-10-15 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 13.9, 63, 258, '2026-10-16 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 60, 477, '2026-10-16 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 741, '2026-10-16 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.5, 54, 796, '2026-10-16 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 488, '2026-10-16 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 59, 214, '2026-10-16 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 11.9, 67, 234, '2026-10-17 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 63, 544, '2026-10-17 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.3, 60, 804, '2026-10-17 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.5, 54, 801, '2026-10-17 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.8, 59, 524, '2026-10-17 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 58, 286, '2026-10-17 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.4, 59, 250, '2026-10-18 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 55, 533, '2026-10-18 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.3, 50, 749, '2026-10-18 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 24, 48, 766, '2026-10-18 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.8, 54, 534, '2026-10-18 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.1, 53, 241, '2026-10-18 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.4, 54, 253, '2026-10-19 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.1, 50, 474, '2026-10-19 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.3, 48, 778, '2026-10-19 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 26, 46, 783, '2026-10-19 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.3, 50, 520, '2026-10-19 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.6, 50, 213, '2026-10-19 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 62, 230, '2026-10-20 08:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.6, 52, 471, '2026-10-20 10:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 53, 751, '2026-10-20 12:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.5, 48, 747, '2026-10-20 14:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 527, '2026-10-20 16:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 220, '2026-10-20 18:00:00')
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 58, 258, DATEADD(hour, 8, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.1, 53, 475, DATEADD(hour, 10, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 761, DATEADD(hour, 12, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.5, 49, 733, DATEADD(hour, 14, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 54, 528, DATEADD(hour, 16, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 283, DATEADD(hour, 18, DATEADD(day, -7, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.9, 59, 261, DATEADD(hour, 8, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 53, 499, DATEADD(hour, 10, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 764, DATEADD(hour, 12, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.5, 47, 778, DATEADD(hour, 14, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.8, 52, 503, DATEADD(hour, 16, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 56, 218, DATEADD(hour, 18, DATEADD(day, -6, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 13.9, 63, 258, DATEADD(hour, 8, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 60, 477, DATEADD(hour, 10, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.8, 51, 741, DATEADD(hour, 12, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.5, 54, 796, DATEADD(hour, 14, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 488, DATEADD(hour, 16, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.6, 59, 214, DATEADD(hour, 18, DATEADD(day, -5, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 11.9, 67, 234, DATEADD(hour, 8, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 63, 544, DATEADD(hour, 10, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.3, 60, 804, DATEADD(hour, 12, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.5, 54, 801, DATEADD(hour, 14, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.8, 59, 524, DATEADD(hour, 16, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.1, 58, 286, DATEADD(hour, 18, DATEADD(day, -4, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 16.4, 59, 250, DATEADD(hour, 8, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.6, 55, 533, DATEADD(hour, 10, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.3, 50, 749, DATEADD(hour, 12, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 24, 48, 766, DATEADD(hour, 14, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.8, 54, 534, DATEADD(hour, 16, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.1, 53, 241, DATEADD(hour, 18, DATEADD(day, -3, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.4, 54, 253, DATEADD(hour, 8, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.1, 50, 474, DATEADD(hour, 10, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 22.3, 48, 778, DATEADD(hour, 12, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 26, 46, 783, DATEADD(hour, 14, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 23.3, 50, 520, DATEADD(hour, 16, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 20.6, 50, 213, DATEADD(hour, 18, DATEADD(day, -2, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 15.9, 62, 230, DATEADD(hour, 8, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 18.6, 52, 471, DATEADD(hour, 10, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 53, 751, DATEADD(hour, 12, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 21.5, 48, 747, DATEADD(hour, 14, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 19.3, 55, 527, DATEADD(hour, 16, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
INSERT INTO EnvironmentData (Fire, Smoke, Temperature, Humidity, Light, _Time) VALUES (0, 0, 17.1, 57, 220, DATEADD(hour, 18, DATEADD(day, -1, CONVERT(varchar(10), GETDATE(), 120))))
+41
View File
@@ -806,3 +806,44 @@ test.skipIf(config.sandbox === 'none')(
},
30000,
)
test('运行时异常:常见的 .NET 异常配中文说明,带出错的文件、行号和方法', async () => {
const { runner } = await runMain(
'exception',
`
Console.WriteLine("start");
int n = int.Parse("abc");`,
)
const error = await runner.next('exception')
expect(error.message).toStartWith('文字转换成数字或日期时格式不对')
expect(error.message).toContain('System.FormatException')
expect(error).toMatchObject({ file: 'Program.cs', line: 11 })
expect(error.detail).toBe('出错位置:Program.cs 第 11 行(Main 方法)')
}, 30000)
test('数据库:DATEADD、DATEDIFF、DATEPART 的日期单位写成关键字(day、hour)', async () => {
const dbDir = resolve(ROOT, '.runs', 'test-dateadd-db')
await rm(dbDir, { recursive: true, force: true })
await mkdir(dbDir, { recursive: true })
const { runner } = await runMain(
'dateadd',
`
var master = new SqlConnection("Data Source=.;User ID=sa");
master.Open();
new SqlCommand("CREATE DATABASE T", master).ExecuteNonQuery();
var conn = new SqlConnection("Data Source=.;Initial Catalog=T");
conn.Open();
foreach (var sql in new[] {
"SELECT DATEADD(hour, 8, DATEADD(day, -7, '2024-01-16'))",
"SELECT DATEDIFF(day, '2024-01-01 23:00', '2024-01-02 01:00')",
"SELECT DATEPART(hh, '2024-01-16 09:05:03')",
})
Console.WriteLine("result=" + new SqlCommand(sql, conn).ExecuteScalar());`,
'using System.Data.SqlClient;',
dbDir,
)
await runner.finished()
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
expect(runner.messages.filter((m) => m.type === 'exception')).toEqual([])
expect(output).toBe('result=2024/1/9 8:00:00\nresult=1\nresult=9\n')
}, 30000)
+2 -1
View File
@@ -120,7 +120,8 @@ export type RunnerMessage =
/** 学生程序操作了实训台输出:led/text、adam4150/do、zigbee/relay、camera/on、camera/pan,data 见 DeviceCtrlHelper.cs、IPCameraHelper.cs */
| { type: 'device'; device: string; action: string; data: unknown }
| { type: 'console'; text: string }
| { type: 'exception'; message: string; detail: string }
/** 学生程序没有处理的异常;file、line 是出错的那行学生代码(找得到时才有),网页上点击可以跳过去 */
| { type: 'exception'; message: string; detail: string; file?: string; line?: number }
| { type: 'exited'; code: number }
/**
+1
View File
@@ -26,6 +26,7 @@ export async function login(username: string, password: string) {
const result = await api<Tokens & { user: User }>('POST', '/api/login', { username, password })
setTokens(result)
currentUser.value = result.user
return result.user
}
export async function logout() {
+34 -9
View File
@@ -373,15 +373,24 @@ function onDrop(event: DragEvent) {
place(d, controlDef(type), x, y)
}
/** 双击工具箱:放在选中的容器(或窗体)左上角 */
/** 双击工具箱:放在选中的容器(选中的是普通控件时放进它所在的容器)左上角,已有控件在那里就往右下错开 */
function onToolDoubleClick(def: ControlDef) {
const d = parsed.value.doc
if (!d) return
// 直接放进选中的容器,不按坐标判断(左上角可能正好压在另一个分组框上)
const container = !def.component && selected.value && isContainerType(typeOf(d, selected.value) ?? '') ? selected.value : ''
const result = addControl(d, def, container, { x: 12, y: container ? 20 : 12 })
// 直接放进容器,不按坐标判断(左上角可能正好压在另一个分组框上)
let container = ''
if (!def.component && selected.value) {
container = isContainerType(typeOf(d, selected.value) ?? '') ? selected.value : (listControls(d).find((c) => c.name === selected.value)?.parent ?? '')
}
const location = { x: 12, y: container ? 20 : 12 }
const taken = childrenOf(d, container).map((child) => localRect(d, child))
while (taken.some((r) => r.x === location.x && r.y === location.y)) {
location.x += 12
location.y += 12
}
const result = addControl(d, def, container, location)
selected.value = result.name
commit(result.doc)
commit(fitInside(result.doc, result.name, container))
}
function place(d: DesignerDocument, def: ControlDef, x: number, y: number) {
@@ -389,7 +398,15 @@ function place(d: DesignerDocument, def: ControlDef, x: number, y: number) {
const origin = container ? absRect(d, container) : { x: 0, y: 0 }
const result = addControl(d, def, container, { x: x - origin.x, y: y - origin.y })
selected.value = result.name
commit(result.doc)
commit(def.component ? result.doc : fitInside(result.doc, result.name, container))
}
/** 新控件比容器宽(LED 显示屏默认 800 宽,新窗体只有 300):按比例缩小到右边留出边距,不用学生再去改大小 */
function fitInside(d: DesignerDocument, name: string, container: string): DesignerDocument {
const rect = localRect(d, name)
const room = (container ? localRect(d, container).width : clientSize.value.width) - rect.x - 12
if (rect.width <= room || room < 40) return d
return setProp(d, name, 'Size', { kind: 'size', width: room, height: Math.max(20, Math.round((rect.height * room) / rect.width)) })
}
// ---------- 属性窗口的回调 ----------
@@ -469,6 +486,7 @@ function onComponentDoubleClick(name: string) {
}
const formTitle = computed(() => String(tree.value?.props.Text ?? ''))
const borderless = computed(() => tree.value?.props.FormBorderStyle === 'None')
const formStyle = computed(() => ({
width: `${clientSize.value.width}px`,
height: `${clientSize.value.height}px`,
@@ -511,7 +529,9 @@ const noop = () => {}
<div class="actions">
<button :disabled="!canUndo" title="撤销(Ctrl+Z)" @click="emit('undo')">↶ 撤销</button>
<button :disabled="!canRedo" title="重做(Ctrl+Y)" @click="emit('redo')">↷ 重做</button>
<span class="tips">拖动工具箱里的控件到窗体上;双击控件生成事件处理方法;Delete 删除</span>
<span class="tips" title="拖动工具箱里的控件到窗体上;双击控件生成事件处理方法;Delete 删除"
>拖动工具箱里的控件到窗体上;双击控件生成事件处理方法;Delete 删除</span
>
<select
class="zoom"
title="窗体显示的大小(不影响窗体的实际大小)"
@@ -524,8 +544,9 @@ const noop = () => {}
</select>
<button class="view-code" title="设计器根据窗体上的控件自动生成的代码(Designer.cs)" @click="emit('openDesignerFile')">查看生成的代码</button>
</div>
<div class="window" :class="{ selected: selected === '' }" :style="zoom !== 1 ? { zoom } : undefined">
<div class="titlebar" @pointerdown="selected = ''">
<div class="window" :class="{ selected: selected === '', borderless }" :style="zoom !== 1 ? { zoom } : undefined">
<!-- 无边框窗体和 VS 设计器里一样不画标题栏,点窗体空白处选中窗体 -->
<div v-if="!borderless" class="titlebar" @pointerdown="selected = ''">
<span class="title">{{ displayText(formTitle) }}</span>
<span class="buttons">─ ☐ ✕</span>
</div>
@@ -707,6 +728,10 @@ const noop = () => {}
box-shadow: 0 2px 10px rgb(0 0 0 / 0.15);
}
.window.borderless {
border-color: transparent;
}
.window.selected {
outline: 1px dashed #2563eb;
outline-offset: 2px;
+2 -2
View File
@@ -7,7 +7,7 @@ import ChartDialog from './ChartDialog.vue'
import type { DesignerDocument } from './document'
import { checkName, getEventHandler, getItems, getPropText, typeOf } from './edit'
import { resourceNameOf } from './resources'
import { NAMED_COLORS, hexToRgb, parseValue, rgb, type Value } from './values'
import { COLOR_LABELS, NAMED_COLORS, hexToRgb, parseValue, rgb, type Value } from './values'
const props = defineProps<{
doc: DesignerDocument
@@ -289,7 +289,7 @@ function renameHandler(event: string, value: string) {
<i class="swatch" :style="{ background: color(prop)?.css ?? 'transparent' }" />
<select :value="color(prop)?.name ?? (color(prop) ? 'custom' : '')" @change="setColor(prop, ($event.target as HTMLSelectElement).value)">
<option value="">(默认)</option>
<option v-for="(_, name) in NAMED_COLORS" :key="name" :value="name">{{ name }}</option>
<option v-for="(_, name) in NAMED_COLORS" :key="name" :value="name" :title="name">{{ COLOR_LABELS[name] ?? name }}</option>
<option v-if="color(prop) && !color(prop)!.name" value="custom" disabled>{{ color(prop)!.css }}</option>
</select>
<input
+3 -3
View File
@@ -9,7 +9,7 @@ test('插入事件处理方法:放在类的最后,与上一个方法空一
const code = await Bun.file(FORM1).text()
const { code: next, line } = ensureHandler(code, 'Form1', 'Form1_Load', 'EventArgs')
const lines = next.split('\n')
expect(lines.slice(line - 4, line + 1)).toEqual(['', ' private void Form1_Load(object sender, EventArgs e)', ' {', '', ' }'])
expect(lines.slice(line - 4, line + 1)).toEqual(['', ' private void Form1_Load(object sender, EventArgs e)', ' {', ' ', ' }'])
// 方法的 "}" 之后就是类的 "}"
expect(lines[line + 1]).toBe(' }')
})
@@ -25,7 +25,7 @@ test('类里还没有成员时不留多余空行', () => {
const code = 'namespace A\n{\n public partial class F : Form\n {\n }\n}\n'
const { code: next, line } = ensureHandler(code, 'F', 'F_Load', 'EventArgs')
expect(next).toBe(
'namespace A\n{\n public partial class F : Form\n {\n private void F_Load(object sender, EventArgs e)\n {\n\n }\n }\n}\n',
'namespace A\n{\n public partial class F : Form\n {\n private void F_Load(object sender, EventArgs e)\n {\n \n }\n }\n}\n',
)
expect(line).toBe(7)
})
@@ -33,5 +33,5 @@ test('类里还没有成员时不留多余空行', () => {
test('字符串和注释里的大括号不影响查找类的结尾', () => {
const code = 'class F\n{\n string s = "}{";\n // }\n char c = \'}\';\n}\n'
const { code: next } = ensureHandler(code, 'F', 'F_Load', 'EventArgs')
expect(next.endsWith(" char c = '}';\n\n private void F_Load(object sender, EventArgs e)\n {\n\n }\n}\n")).toBe(true)
expect(next.endsWith(" char c = '}';\n\n private void F_Load(object sender, EventArgs e)\n {\n \n }\n}\n")).toBe(true)
})
+3 -1
View File
@@ -20,7 +20,9 @@ export function ensureHandler(code: string, className: string, handler: string,
// 类里已有成员时,与上一个成员之间空一行
const before = code.slice(0, lineStart)
const hasMembers = !/\{\s*$/.test(before)
const method = [...(hasMembers ? [''] : []), `${indent}private void ${handler}(object sender, ${args} e)`, `${indent}{`, '', `${indent}}`].join(eol)
// 方法体的空行带好缩进(和 VS 一样),光标跳过去就在缩进后面,学生的代码不会顶格写
const body = `${indent} `
const method = [...(hasMembers ? [''] : []), `${indent}private void ${handler}(object sender, ${args} e)`, `${indent}{`, body, `${indent}}`].join(eol)
const next = `${before}${method}${eol}${code.slice(lineStart)}`
return { code: next, line: lineOf(next, before.length) + (hasMembers ? 3 : 2) }
}
+24
View File
@@ -134,6 +134,30 @@ export const NAMED_COLORS: Record<string, string> = {
Cyan: '#00ffff',
}
/** 属性窗口里颜色的中文名,英文名放在悬停提示里 */
export const COLOR_LABELS: Record<string, string> = {
Transparent: '透明',
Black: '黑色',
White: '白色',
Red: '红色',
Green: '绿色',
Blue: '蓝色',
Yellow: '黄色',
Orange: '橙色',
Gray: '灰色',
DimGray: '暗灰色',
Silver: '银色',
LightGray: '浅灰色',
DarkBlue: '深蓝色',
SkyBlue: '天蓝色',
LightGreen: '浅绿色',
Lime: '亮绿色',
Purple: '紫色',
Brown: '棕色',
Pink: '粉红色',
Cyan: '青色',
}
/** 系统颜色(System.Drawing.SystemColors),按 Windows 10 默认主题取值 */
const SYSTEM_COLORS: Record<string, string> = {
Control: '#f0f0f0',
+2
View File
@@ -124,6 +124,8 @@ onMounted(() => {
tabSize: 4,
insertSpaces: true,
minimap: { enabled: false },
// 顶部不固定显示所在的类和方法:点报错跳到文件开头附近的行时,那一行会被它挡住
stickyScroll: { enabled: false },
scrollBeyondLastLine: false,
renderWhitespace: 'selection',
unicodeHighlight: { ambiguousCharacters: false },
+2
View File
@@ -1,5 +1,7 @@
// 按需加载 Monaco(0.57 的模块布局):编辑器 API + 全部编辑功能(查找、折叠、括号匹配等)+ C# 和 SQL 语法高亮,
// 不加载其他几十种语言和 JS/TS/CSS 语言服务,体积小很多。
// 中文界面(查找替换、右键菜单、提示文字):必须在编辑器模块之前加载,编辑器模块加载时就会取界面文字
import 'monaco-editor/nls/lang/zh-cn'
import * as monaco from 'monaco-editor/editor/editor.api'
import 'monaco-editor/features/register.all'
import 'monaco-editor/languages/definitions/csharp/register'
+8
View File
@@ -77,6 +77,7 @@ const formatTime = (ms: number) => new Date(ms).toLocaleTimeString('zh-CN', { ho
:show-mask="false"
:trap-focus="false"
:block-scroll="false"
class="task-drawer"
@update:show="emit('update:show', $event)"
>
<NDrawerContent title="任务" closable>
@@ -117,6 +118,13 @@ const formatTime = (ms: number) => new Date(ms).toLocaleTimeString('zh-CN', { ho
</NDrawer>
</template>
<style>
/* 从顶部工具栏下面开始,不挡住"我的作业""数据库""退出"等链接 */
.n-drawer.task-drawer {
top: 49px;
}
</style>
<style scoped>
.task {
font-size: 14px;
+3 -2
View File
@@ -10,7 +10,7 @@ import EditTab from '../database/EditTab.vue'
import ObjectExplorer, { type ExplorerAction } from '../database/ObjectExplorer.vue'
import QueryTab from '../database/QueryTab.vue'
import { execute, loadExplorer, quoteName, type ExplorerDatabase, type ExplorerTable } from '../database/sql'
import { renderSampleTag, sampleOptions } from '../samples'
import { lastSample, renderSampleTag, sampleOptions } from '../samples'
type Tab =
| { key: string; kind: 'query'; title: string; database: string; sql?: string; run?: boolean }
@@ -148,7 +148,8 @@ const restoreBusy = ref(false)
async function openRestore() {
experiments.value = await api('GET', '/api/db/experiments')
restoring.value = experiments.value[0]?.id ?? null
// 默认选刚才做的实验
restoring.value = experiments.value.find((e) => e.id === lastSample())?.id ?? experiments.value[0]?.id ?? null
}
async function restore() {
+2 -2
View File
@@ -28,8 +28,8 @@ import { currentUser } from '../auth'
<h3>2. 自由练习和做作业</h3>
<ul>
<li>登录后进入<b>自由练习</b>:左上角的下拉框选教材上的任务,可以随便改、随便运行。</li>
<li>点右上角的<b>我的作业</b>,看老师布置的作业、截止时间和得分;点作业名开始做。</li>
<li>登录后先看到<b>我的作业</b>:老师布置的作业、截止时间和得分;点作业名开始做。做实验时随时可以点右上角的<b>我的作业</b>回来。</li>
<li>点<b>自由练习</b>:左上角的下拉框选教材上的任务,可以随便改、随便运行;下次打开时回到上次做的任务。</li>
<li>工具栏的<b>任务</b>:任务目标、控件名、实现步骤和在平台上怎么试,第一次打开一个实验时自动弹出。面板不挡住设计器和代码,可以开着做。</li>
<li>
作业的起点有两种:<b>界面已做好</b>(只写事件和代码)、<b>空白工程</b>(从搭界面做起)。控件名照书上的写,自动评分按书上的控件名操作你的程序。
+4 -2
View File
@@ -17,8 +17,10 @@ async function submit() {
busy.value = true
error.value = ''
try {
await login(username.value.trim(), password.value)
const redirect = typeof route.query.redirect === 'string' && route.query.redirect.startsWith('/') ? route.query.redirect : '/'
const user = await login(username.value.trim(), password.value)
// 学生登录后先看作业列表,老师进实验页
const fallback = user.role === 'student' ? '/assignments' : '/'
const redirect = typeof route.query.redirect === 'string' && route.query.redirect.startsWith('/') ? route.query.redirect : fallback
await router.replace(redirect)
} catch (e) {
error.value = (e as Error).message
+4 -3
View File
@@ -15,7 +15,7 @@ import RunArea from '../run/RunArea.vue'
import { createRunSession } from '../run/session'
import GradeResultView from '../grading/GradeResultView.vue'
import { conventionHints } from '@iot/shared'
import { downloadCsv } from '../teacher/csv'
import { downloadCsv, textCell } from '../teacher/csv'
interface Row {
userId: number
@@ -270,7 +270,7 @@ function exportCsv() {
'评语',
],
...rows.value.map((r) => [
`\t${r.username}`,
textCell(r.username),
r.status ? (r.autoScore ?? '') : '未提交',
...a.scoring.manual.map((_, i) => r.review?.scores[i] ?? ''),
r.status ? (r.autoScore ?? 0) + (r.manualScore ?? 0) : '',
@@ -293,7 +293,8 @@ async function signOut() {
<strong>{{ assignment?.title }}</strong>
<span class="muted">{{ assignment?.className }}</span>
<span class="muted">已提交 {{ rows.filter((r) => r.status).length }} / {{ rows.length }} 人</span>
<NButton size="small" @click="adoptAll">全部采纳 AI 建议</NButton>
<!-- 没配置 AI 评阅时不会有 AI 建议,不显示这个按钮 -->
<NButton v-if="rows.some((r) => r.aiStatus)" size="small" @click="adoptAll">全部采纳 AI 建议</NButton>
<NButton size="small" @click="exportCsv">导出成绩</NButton>
<span class="spacer" />
<span class="user">{{ currentUser?.name }}</span>
+2 -2
View File
@@ -7,7 +7,7 @@ import { api } from '../api'
import { currentUser, logout } from '../auth'
import AssignmentsPanel from '../teacher/AssignmentsPanel.vue'
import GradebookPanel from '../teacher/GradebookPanel.vue'
import { downloadCsv } from '../teacher/csv'
import { downloadCsv, textCell } from '../teacher/csv'
import { decodeCsv, parseCsv, toRoster, type RosterRow } from '../teacher/roster'
interface ClassInfo {
@@ -177,7 +177,7 @@ async function confirmImport() {
function exportRoster() {
if (!selected.value) return
downloadCsv(`${selected.value.name}-名单.csv`, [['用户名', '密码'], ...students.value.map((s) => [`\t${s.username}`, `\t${s.password}`])])
downloadCsv(`${selected.value.name}-名单.csv`, [['用户名', '密码'], ...students.value.map((s) => [textCell(s.username), textCell(s.password)])])
}
function downloadTemplate() {
+13 -11
View File
@@ -30,7 +30,7 @@ import { createRunSession } from '../run/session'
import GradeResultView from '../grading/GradeResultView.vue'
import TaskPanel from '../grading/TaskPanel.vue'
import { captureForms } from '../grading/screenshots'
import { renderSampleTag, sampleOptions } from '../samples'
import { lastSample, rememberSample, renderSampleTag, sampleOptions } from '../samples'
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
@@ -128,8 +128,8 @@ onMounted(async () => {
}
const list = await api<{ id: string; title: string }[]>('GET', '/api/samples')
samples.value = sampleOptions(list)
// 网址里带 ?sample=示例编号 时直接打开该示例(便于老师把链接发给学生)
const initial = list.find((s) => s.id === route.query.sample) ?? list[0]
// 网址里带 ?sample=示例编号 时直接打开该示例(便于老师把链接发给学生),否则打开上次做的实验
const initial = list.find((s) => s.id === route.query.sample) ?? list.find((s) => s.id === lastSample()) ?? list[0]
if (initial) await loadSample(initial.id)
})
@@ -150,6 +150,7 @@ async function openProject(key: string, original: () => Promise<SourceFile[]>, d
async function loadSample(id: string, discardWork = false) {
sampleId.value = id
void router.replace({ query: { sample: id } })
rememberSample(id)
sampleChecks.value = []
sampleTask.value = null
void api<string[]>('GET', `/api/samples/${encodeURIComponent(id)}/checks`)
@@ -596,7 +597,7 @@ function run() {
:before-start="flushSave"
/>
<main class="workspace" :class="{ designing: activeForm, 'run-collapsed': runCollapsed }">
<main class="workspace" :class="{ 'run-collapsed': runCollapsed }">
<section class="editor">
<div class="tab-row">
<NTabs v-model:value="activeTab" type="card" size="small" class="file-tabs">
@@ -698,7 +699,7 @@ function run() {
}
.sample-select {
width: 240px;
width: 300px;
}
.metrics {
@@ -758,16 +759,12 @@ function run() {
white-space: nowrap;
}
/* 设计页要放工具箱、窗体和属性窗口,代码页要放得下长代码行,左栏都放宽;切换标签页时布局不跳 */
.workspace {
flex: 1;
display: grid;
grid-template-columns: minmax(360px, 2fr) 14px 3fr;
min-height: 0;
}
/* 设计器需要工具箱、窗体和属性窗口,左栏放宽 */
.workspace.designing {
grid-template-columns: minmax(0, 3fr) 14px minmax(360px, 2fr);
min-height: 0;
}
.workspace.run-collapsed {
@@ -815,6 +812,11 @@ function run() {
min-width: 0;
}
/* 文件多时(项目 8 有十来个)标签换行显示,不藏在看不到滚动条的横向滚动里 */
.file-tabs :deep(.n-tabs-wrapper) {
flex-wrap: wrap;
}
.new-form {
display: flex;
flex-direction: column;
+116 -60
View File
@@ -11,7 +11,9 @@ import MessageBoxDialog from '../winforms/MessageBoxDialog.vue'
import type { RunSession } from './session'
const props = defineProps<{ session: RunSession }>()
const emit = defineEmits<{ diagnostic: [file: string, line: number, column: number] }>()
const emit = defineEmits<{
diagnostic: [file: string, line: number, column: number]
}>()
const state = props.session.state
provide(RUN_IMAGES, props.session.images)
@@ -26,7 +28,10 @@ const maximized = ref(false)
const desktop = ref<HTMLElement>()
const desktopSize = ref({ width: 0, height: 0 })
const observer = new ResizeObserver(([entry]) => {
desktopSize.value = { width: entry.contentRect.width, height: entry.contentRect.height }
desktopSize.value = {
width: entry.contentRect.width,
height: entry.contentRect.height,
}
})
onMounted(() => observer.observe(desktop.value!))
onBeforeUnmount(() => observer.disconnect())
@@ -39,7 +44,10 @@ const scale = computed(() => {
state.forms.forEach((form, index) => {
const p = form.root.props
if (p.Visible === false) return
const client = (p.ClientSize as SizeValue | undefined) ?? { width: 300, height: 300 }
const client = (p.ClientSize as SizeValue | undefined) ?? {
width: 300,
height: 300,
}
const start = WINDOW_MARGIN + index * WINDOW_STEP
width = Math.max(width, start + client.width + 2 + WINDOW_MARGIN)
height = Math.max(height, start + client.height + 2 + (p.FormBorderStyle === 'None' ? 0 : TITLEBAR) + WINDOW_MARGIN)
@@ -58,53 +66,54 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
<template>
<section class="run-area" :class="{ maximized }">
<div ref="desktop" class="desktop">
<div class="desktop-canvas" :style="scale < 1 ? { transform: `scale(${scale})`, width: `${100 / scale}%`, height: `${100 / scale}%` } : undefined">
<FormWindow
v-for="(form, index) in state.forms"
:key="form.root.id"
:form="form.root"
:modal="form.modal"
:offset="index"
:scale="scale"
:act="session.act"
/>
</div>
<MessageBoxDialog v-for="box in state.messageBoxes" :key="box.requestId" :box="box" @reply="session.replyMessageBox(box.requestId, $event)" />
<div v-if="state.connection === 'lost'" class="desktop-hint lost">与服务器的连接断开了,正在重新连接…</div>
<div v-else-if="state.connection === 'restored' && state.forms.length === 0" class="desktop-hint lost">
连接已恢复。刚才的程序随连接断开已经结束,请重新运行
</div>
<div v-else-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
<div class="stage">
<div class="desktop-tools">
<span v-if="scale < 1" class="desktop-scale" title='窗体比运行区大,按比例缩小显示;点"放大"可以看得更清楚'> 缩放 {{ Math.round(scale * 100) }}% </span>
<span v-if="scale < 1" class="desktop-scale" title='窗体比运行区大,按比例缩小显示;点"放大"可以看得更清楚'>缩放 {{ Math.round(scale * 100) }}%</span>
<button
v-if="maximized && (state.status === 'compiling' || state.status === 'running')"
type="button"
title="结束正在运行的程序(Shift+F5)"
@click="session.stop()"
>
■ 停止
</button>
<button type="button" :title="maximized ? '还原(Esc)' : '运行区占满整个网页'" @click="maximized = !maximized">
{{ maximized ? '还原' : '放大' }}
</button>
</div>
</div>
<TrainingBoard :board="state.board" :playing="state.playing" @set="session.setBoard" @reset="session.resetBoard" />
<div v-if="state.savedImages.length" class="photos">
<div class="photos-title">程序保存的图片({{ state.savedImages.length }})<span>只在这次运行中保留</span></div>
<div class="photos-list">
<figure
v-for="(photo, i) in state.savedImages"
:key="i"
:title="`${photo.path}(点击查看大图)`"
@click="viewing = { path: photo.path, url: session.images.get(photo.image) ?? '' }"
<div ref="desktop" class="desktop">
<div
class="desktop-canvas"
:style="
scale < 1
? {
transform: `scale(${scale})`,
width: `${100 / scale}%`,
height: `${100 / scale}%`,
}
: undefined
"
>
<img :src="session.images.get(photo.image)" alt="" />
<figcaption>{{ photo.path }}</figcaption>
</figure>
<FormWindow
v-for="(form, index) in state.forms"
:key="form.root.id"
:form="form.root"
:modal="form.modal"
:offset="index"
:scale="scale"
:act="session.act"
/>
</div>
<MessageBoxDialog v-for="box in state.messageBoxes" :key="box.requestId" :box="box" @reply="session.replyMessageBox(box.requestId, $event)" />
<div v-if="state.connection === 'lost'" class="desktop-hint lost">与服务器的连接断开了,正在重新连接…</div>
<div v-else-if="state.connection === 'restored' && state.forms.length === 0" class="desktop-hint lost">
连接已恢复。刚才的程序随连接断开已经结束,请重新运行
</div>
<div v-else-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
</div>
</div>
<div v-if="viewing" class="photo-viewer" @click="viewing = null">
<figure>
<img :src="viewing.url" alt="" />
<figcaption>{{ viewing.path }}(点击任意处关闭)</figcaption>
</figure>
</div>
<div class="output">
<!-- 编译错误、异常、控制台输出和保存的图片紧跟在桌面下面,不会被很高的实训台挤到看不见的地方 -->
<div v-if="state.diagnostics.length || state.errors.length || state.console" class="output">
<div
v-for="(d, i) in state.diagnostics"
:key="`d${i}`"
@@ -115,9 +124,41 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
{{ d.file }}({{ d.line }},{{ d.column }}) {{ d.code }}:{{ d.message }}
<div v-if="d.hint" class="hint">提示:{{ d.hint }}</div>
</div>
<pre v-for="(e, i) in state.errors" :key="`e${i}`" class="error">{{ e }}</pre>
<pre
v-for="(e, i) in state.errors"
:key="`e${i}`"
:class="['error', { jump: e.file && e.line }]"
:title="e.file && e.line ? '点击跳到出错的代码' : undefined"
@click="e.file && e.line && emit('diagnostic', e.file, e.line, 1)"
>{{ e.text }}</pre>
<pre v-if="state.console" class="console">{{ state.console }}</pre>
</div>
<div v-if="state.savedImages.length" class="photos">
<div class="photos-title">程序保存的图片({{ state.savedImages.length }})<span>只在这次运行中保留</span></div>
<div class="photos-list">
<figure
v-for="(photo, i) in state.savedImages"
:key="i"
:title="`${photo.path}(点击查看大图)`"
@click="
viewing = {
path: photo.path,
url: session.images.get(photo.image) ?? '',
}
"
>
<img :src="session.images.get(photo.image)" alt="" />
<figcaption>{{ photo.path }}</figcaption>
</figure>
</div>
</div>
<TrainingBoard :board="state.board" :playing="state.playing" @set="session.setBoard" @reset="session.resetBoard" />
<div v-if="viewing" class="photo-viewer" @click="viewing = null">
<figure>
<img :src="viewing.url" alt="" />
<figcaption>{{ viewing.path }}(点击任意处关闭)</figcaption>
</figure>
</div>
</section>
</template>
@@ -131,6 +172,18 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
overflow: auto;
}
/* 运行区内容超出高度时整体滚动,除了桌面都不被压缩 */
.run-area > :not(.stage) {
flex-shrink: 0;
}
.stage {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.desktop {
position: relative;
flex: 1;
@@ -151,36 +204,31 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
transform-origin: 0 0;
}
/* 放在桌面上方,不压住学生窗体的标题栏按钮 */
.desktop-tools {
position: absolute;
top: 6px;
right: 6px;
z-index: 100001;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
font-size: 12px;
}
.desktop-scale {
padding: 2px 6px;
border-radius: 3px;
background: rgb(0 0 0 / 0.35);
color: #fff;
color: #6b7280;
}
.desktop-tools button {
padding: 2px 10px;
border: 1px solid rgb(255 255 255 / 0.6);
padding: 1px 10px;
border: 1px solid #d1d5db;
border-radius: 3px;
background: rgb(0 0 0 / 0.35);
color: #fff;
background: #fff;
color: #374151;
font-size: 12px;
cursor: pointer;
}
.desktop-tools button:hover {
background: rgb(0 0 0 / 0.55);
border-color: #9ca3af;
}
/* 放大:桌面在左边占满高度,实训台、图片列表和输出在右边一栏 */
@@ -195,13 +243,13 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
background: #f3f4f6;
}
.run-area.maximized .desktop {
.run-area.maximized .stage {
grid-column: 1;
grid-row: 1 / span 20;
height: calc(100vh - 16px);
}
.run-area.maximized > :not(.desktop) {
.run-area.maximized > :not(.stage) {
grid-column: 2;
}
@@ -293,11 +341,19 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
font-size: 12px;
}
.diag {
/* 长的报错和输出折行显示,不用左右滚动 */
.output pre {
white-space: pre-wrap;
word-break: break-all;
}
.diag,
.error.jump {
cursor: pointer;
}
.diag:hover {
.diag:hover,
.error.jump:hover {
text-decoration: underline;
}
+18 -5
View File
@@ -8,6 +8,12 @@ export interface OpenForm {
}
/** 学生程序保存的图片(如摄像头照片),path 相对程序所在目录 */
export interface RunError {
text: string
file?: string
line?: number
}
export interface SavedImage {
path: string
image: string
@@ -27,7 +33,8 @@ export function createRunSession() {
status: 'idle' as 'idle' | 'compiling' | 'running' | 'stopped',
diagnostics: [] as Diagnostic[],
console: '',
errors: [] as string[],
/** 运行出错、连接断开等提示;运行时异常带着出错的文件和行号,点击跳过去 */
errors: [] as RunError[],
forms: [] as OpenForm[],
messageBoxes: [] as PendingMessageBox[],
/** 虚拟实训台,由服务器推送;连接建立前为 null */
@@ -103,7 +110,7 @@ export function createRunSession() {
ws.onclose = () => {
// 服务器那边的程序随连接一起结束了;窗体留在页面上只会"点了没反应"
if (!closed && (state.forms.length > 0 || state.status === 'compiling' || state.status === 'running')) {
state.errors.push('与服务器的连接断开了(网络不稳或服务器正在更新),程序已经结束。连上之后请重新运行。')
state.errors.push({ text: '与服务器的连接断开了(网络不稳或服务器正在更新),程序已经结束。连上之后请重新运行。' })
}
stopAudio()
state.forms = []
@@ -131,7 +138,7 @@ export function createRunSession() {
async function run(files: SourceFile[], sampleId?: string) {
await connect()
if (socket?.readyState !== WebSocket.OPEN) {
state.errors = ['连不上服务器,请检查网络,稍后再点"运行"。']
state.errors = [{ text: '连不上服务器,请检查网络,稍后再点"运行"。' }]
return
}
state.connection = 'open'
@@ -194,6 +201,12 @@ export function createRunSession() {
switch (message.type) {
case 'status':
state.status = message.state
// 点"停止"时服务器直接结束进程,不再发 exited;窗体留着只会"点了没反应"
if (message.state === 'stopped') {
stopAudio()
state.forms = []
state.messageBoxes = []
}
break
case 'compiled':
state.diagnostics = message.diagnostics
@@ -259,10 +272,10 @@ export function createRunSession() {
state.console += message.text
break
case 'exception':
state.errors.push(`${message.message}\n${message.detail}`)
state.errors.push({ text: `运行出错:${message.message}\n${message.detail}`, file: message.file, line: message.line })
break
case 'serverError':
state.errors.push(message.message)
state.errors.push({ text: message.message })
break
case 'exited':
stopAudio()
+20 -1
View File
@@ -1,4 +1,4 @@
// 实验下拉菜单:按教材的项目分组,组名是项目名,组里每项只写"任务N …";选中后框里显示完整标题
// 实验下拉菜单:按教材的项目分组,组名是项目名,组里每项只写"任务N …";选中后框里显示完整标题。另外记住上次做的实验
import { h } from 'vue'
import type { SelectGroupOption, SelectOption, SelectRenderTag } from 'naive-ui'
@@ -32,3 +32,22 @@ export function sampleOptions(list: { id: string; title: string }[]): SelectGrou
/** 选择框里显示完整标题(带项目号),下拉列表里仍是简短的"任务N …" */
export const renderSampleTag: SelectRenderTag = ({ option }) => h('span', { title: option.title as string }, option.title as string)
// 记住上次做的实验:从数据库页面返回、重新登录后回到这个实验,恢复数据时默认选它
const LAST_SAMPLE_KEY = 'iot-sim:last-sample'
export function lastSample() {
try {
return localStorage.getItem(LAST_SAMPLE_KEY)
} catch {
return null
}
}
export function rememberSample(id: string) {
try {
localStorage.setItem(LAST_SAMPLE_KEY, id)
} catch {
// 浏览器禁用了本地存储:下次打开第一个实验
}
}
+9 -3
View File
@@ -4,7 +4,7 @@ import { computed, h, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { NButton, NDataTable, NEmpty, useMessage, type DataTableColumns } from 'naive-ui'
import { api } from '../api'
import { downloadCsv } from './csv'
import { downloadCsv, textCell } from './csv'
interface Cell {
submitted: boolean
@@ -61,11 +61,14 @@ const columns = computed<DataTableColumns<Row>>(() => [
{ title: `合计(${fullMarks.value})`, key: 'sum', width: 110, sorter: (a: Row, b: Row) => sum(a) - sum(b), render: (row: Row) => String(sum(row)) },
])
/** 有 AI 建议分时才解释"AI 建议,待确认"(没配置 AI 评阅时不会出现) */
const hasAi = computed(() => book.value.students.some((s) => Object.values(s.scores).some((c) => c.aiManual !== null)))
function exportCsv() {
downloadCsv(`${props.className}-成绩表.csv`, [
['用户名', ...book.value.assignments.map((a) => `${a.title}(${a.total})`), `合计(${fullMarks.value})`],
...book.value.students.map((s) => [
`\t${s.username}`,
textCell(s.username),
...book.value.assignments.map((a) => (s.scores[a.id]?.submitted ? s.scores[a.id].total : '未交')),
sum(s),
]),
@@ -76,7 +79,10 @@ function exportCsv() {
<template>
<div>
<div class="head">
<span class="tip">总分 = 最近一次提交的自动评分 + 老师评分;"AI 建议,待确认"是含 AI 建议分的总分,老师采纳后才算数;"待评"只含自动评分。</span>
<span class="tip"
>总分 = 最近一次提交的自动评分 + 老师评分;<template v-if="hasAi">"AI 建议,待确认"是含 AI 建议分的总分,老师采纳后才算数;</template
>"待评"只含自动评分。</span
>
<NButton size="small" :disabled="book.students.length === 0" @click="exportCsv">导出成绩表(CSV)</NButton>
</div>
<NDataTable
+4 -1
View File
@@ -1,5 +1,5 @@
// 下载 CSV:开头带 BOM,Excel 打开时中文不乱码。
// 账号、密码等长数字前加制表符(cell 以 \t 开头),Excel 不会把它变成科学计数法。
// 账号、密码这类纯数字的文字用 textCell 包一下,Excel 不会把它变成数字(丢掉开头的 0、变成科学计数法)。
export function downloadCsv(filename: string, rows: (string | number | null)[][]) {
const quote = (value: string | number | null) => {
const cell = value === null ? '' : String(value)
@@ -12,3 +12,6 @@ export function downloadCsv(filename: string, rows: (string | number | null)[][]
link.click()
URL.revokeObjectURL(link.href)
}
/** 纯数字时前面加制表符,Excel 按文字处理;姓名等其他文字原样输出,不留多余的空白 */
export const textCell = (value: string) => (/^\d+$/.test(value) ? `\t${value}` : value)
+37 -7
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
// 按控件类型渲染一个 WinForms 控件及其子控件。
// 用户操作只通过 act() 上报,由运行器按真实 WinForms 的顺序触发事件。
import { computed, inject, provide, ref, type CSSProperties } from 'vue'
import { computed, inject, onBeforeUnmount, provide, ref, watch, type CSSProperties } from 'vue'
import type { ControlNode, UserAction } from '@iot/shared'
import { AMBIENT_BACK_COLOR, AMBIENT_BACK_TYPES, boxCss, DEFAULT_BACK_COLOR, displayText } from './format'
import { backgroundCss, imageSources, RUN_IMAGES, srcset, type ImageValue } from './images'
@@ -49,10 +49,12 @@ const passwordChar = computed(() => {
return ch && ch !== '\0' ? ch : ''
})
// ComboBox
// ComboBox:下拉列表挂到页面最外层(和真实 WinForms 一样可以伸出窗体),按控件在屏幕上的位置和缩放摆放
const dropDownOpen = ref(false)
const items = computed(() => (p.value.Items as string[] | undefined) ?? [])
const editable = computed(() => p.value.DropDownStyle !== 'DropDownList')
const combo = ref<HTMLElement>()
const listStyle = ref<CSSProperties>({})
function openDropDown() {
if (!enabled.value) return
@@ -60,11 +62,37 @@ function openDropDown() {
dropDownOpen.value = false
return
}
const el = combo.value
if (!el) return
const rect = el.getBoundingClientRect()
listStyle.value = {
left: `${rect.left}px`,
top: `${rect.bottom}px`,
width: `${el.offsetWidth}px`,
transform: `scale(${rect.width / el.offsetWidth || 1})`,
font: getComputedStyle(el).font,
}
// 先触发 Click(教材在 Click 里填充串口列表),列表随后按最新的 Items 显示
act({ action: 'dropDown' })
dropDownOpen.value = true
}
/** 点列表以外的地方、滚动页面时收起列表 */
function closeOnOutside(event: Event) {
const target = event.target as Node
if (combo.value?.contains(target) || (target instanceof Element && target.closest('.wf-combobox-list'))) return
dropDownOpen.value = false
}
watch(dropDownOpen, (open) => {
const method = open ? 'addEventListener' : 'removeEventListener'
window[method]('mousedown', closeOnOutside, true)
window[method]('scroll', closeOnOutside, true)
})
onBeforeUnmount(() => {
window.removeEventListener('mousedown', closeOnOutside, true)
window.removeEventListener('scroll', closeOnOutside, true)
})
function selectItem(index: number) {
dropDownOpen.value = false
act({ action: 'select', index })
@@ -130,7 +158,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
</div>
<!-- ComboBox -->
<div v-else-if="node.type === 'ComboBox'" class="wf-control wf-combobox" :style="style">
<div v-else-if="node.type === 'ComboBox'" ref="combo" class="wf-control wf-combobox" :style="style">
<input
:value="String(p.Text ?? '')"
:readonly="!editable"
@@ -139,10 +167,12 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
@input="act({ action: 'text', value: ($event.target as HTMLInputElement).value })"
/>
<span class="wf-combobox-arrow" @mousedown.prevent="openDropDown">▾</span>
<ul v-if="dropDownOpen" class="wf-combobox-list">
<li v-for="(item, index) in items" :key="index" @mousedown.prevent="selectItem(index)">{{ item }}</li>
<li v-if="items.length === 0" class="wf-combobox-empty">&nbsp;</li>
</ul>
<Teleport to="body">
<ul v-if="dropDownOpen" class="wf-combobox-list" :style="listStyle">
<li v-for="(item, index) in items" :key="index" @mousedown.prevent="selectItem(index)">{{ item }}</li>
<li v-if="items.length === 0" class="wf-combobox-empty">&nbsp;</li>
</ul>
</Teleport>
</div>
<!-- ListBox:点空白处只触发 Click,点某一项先选中再触发 Click -->
+5 -5
View File
@@ -136,12 +136,12 @@
background: #fff;
}
/* 挂在 body 下,位置、宽度、缩放和字体由 ControlView.vue 按下拉框算好 */
.wf-combobox-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 5;
position: fixed;
z-index: 2000;
box-sizing: border-box;
transform-origin: 0 0;
margin: 0;
padding: 0;
list-style: none;