更新
This commit is contained in:
6202
public/static/all.css
Normal file
6202
public/static/all.css
Normal file
File diff suppressed because it is too large
Load Diff
322
public/static/app.js
Normal file
322
public/static/app.js
Normal file
@@ -0,0 +1,322 @@
|
||||
var autoRefreshIntervalId = null;
|
||||
let loadedSchedule = null;
|
||||
|
||||
const dateTimeFormat = JSJoda.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
const byLinePanel = document.getElementById("byLinePanel");
|
||||
const byLineTimelineOptions = {
|
||||
locale: 'zh-cn',
|
||||
timeAxis: { scale: "hour" },
|
||||
orientation: { axis: "top" },
|
||||
stack: false,// ture则不重叠
|
||||
xss: { disabled: true }, // 显式将此选项设置为true以完全禁用Timeline的XSS保护
|
||||
zoomMin: 1000 * 60 * 60 * 12 // Half day in milliseconds
|
||||
};
|
||||
var byLineGroupDataSet = new vis.DataSet();
|
||||
var byLineItemDataSet = new vis.DataSet();
|
||||
var byLineTimeline = new vis.Timeline(byLinePanel, byLineItemDataSet, byLineGroupDataSet, byLineTimelineOptions);
|
||||
var solveArr = JSON.parse(localStorage.getItem('scheduleArr'))
|
||||
var issolve = localStorage.getItem('solve')
|
||||
|
||||
const byJobPanel = document.getElementById("byJobPanel");
|
||||
var baseUrl = 'http://172.16.32.21:48080/'
|
||||
var baseUrlNew = 'http://172.16.32.21:8081/'
|
||||
const byJobTimelineOptions = {
|
||||
timeAxis: { scale: "hour" },
|
||||
orientation: { axis: "top" },
|
||||
stack: false,
|
||||
xss: { disabled: true }, // Items are XSS safe through JQuery
|
||||
zoomMin: 1000 * 60 * 60 * 12 // Half day in milliseconds
|
||||
};
|
||||
var byJobGroupDataSet = new vis.DataSet();
|
||||
var byJobItemDataSet = new vis.DataSet();
|
||||
var byJobTimeline = new vis.Timeline(byJobPanel, byJobItemDataSet, byJobGroupDataSet, byJobTimelineOptions);
|
||||
|
||||
$(document).ready(function () {
|
||||
replaceTimefoldAutoHeaderFooter();
|
||||
$("#stopSolvingButton").hide();
|
||||
|
||||
$("#refreshButton").click(function () {
|
||||
refreshSchedule();
|
||||
});
|
||||
$("#solveButton").click(function () {
|
||||
solve();
|
||||
});
|
||||
$("#stopSolvingButton").click(function () {
|
||||
stopSolving();
|
||||
});
|
||||
$("#analyzeButton").click(function () {
|
||||
analyze();
|
||||
});
|
||||
// HACK to allow vis-timeline to work within Bootstrap tabs
|
||||
$("#byLineTab").on('shown.bs.tab', function (event) {
|
||||
byLineTimeline.redraw();
|
||||
})
|
||||
$("#byJobTab").on('shown.bs.tab', function (event) {
|
||||
byJobTimeline.redraw();
|
||||
})
|
||||
|
||||
setupAjax();
|
||||
if (issolve === 'true') {
|
||||
$.post(baseUrl + "admin-api/aps/order/load", JSON.stringify(solveArr), function () {
|
||||
refreshSchedule();
|
||||
}).fail(function (xhr, ajaxOptions, thrownError) {
|
||||
showError("Start solving failed.", xhr);
|
||||
});
|
||||
localStorage.setItem('solve', false)
|
||||
}
|
||||
});
|
||||
|
||||
function setupAjax() {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job
|
||||
'Authorization': "Bearer " + localStorage.getItem('ACCESS_TOKEN'),
|
||||
'tenant-id': localStorage.getItem('TENANT_ID'),
|
||||
}
|
||||
});
|
||||
|
||||
// Extend jQuery to support $.put() and $.delete()
|
||||
jQuery.each(["put", "delete"], function (i, method) {
|
||||
jQuery[method] = function (url, data, callback, type) {
|
||||
if (jQuery.isFunction(data)) {
|
||||
type = type || callback;
|
||||
callback = data;
|
||||
data = undefined;
|
||||
}
|
||||
return jQuery.ajax({
|
||||
url: url,
|
||||
type: method,
|
||||
dataType: type,
|
||||
data: data,
|
||||
success: callback
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function refreshSchedule() {
|
||||
$.get(baseUrlNew + "schedule", function (schedule) {
|
||||
// $.get(baseUrl + "admin-api/aps/order/schedule", function (schedule) {
|
||||
refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING");
|
||||
$("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score));
|
||||
loadedSchedule = schedule;
|
||||
const unassignedJobs = $("#unassignedJobs");
|
||||
unassignedJobs.children().remove();
|
||||
var unassignedJobsCount = 0;
|
||||
byLineGroupDataSet.clear();
|
||||
byJobGroupDataSet.clear();
|
||||
byLineItemDataSet.clear();
|
||||
byJobItemDataSet.clear();
|
||||
$.each(schedule.lines, (index, line) => {
|
||||
const lineGroupElement = $(`<div/>`)
|
||||
.append($(`<h5 class="card-title mb-1"/>`).text(line.name))
|
||||
.append($(`<p class="card-text ms-2 mb-0"/>`).text(line.operator));
|
||||
byLineGroupDataSet.add({ id: line.id, content: lineGroupElement.html() });
|
||||
});
|
||||
$.each(schedule.jobs, (index, job) => {
|
||||
byJobGroupDataSet.add({ id: job.id, content: job.name });
|
||||
byJobItemDataSet.add({
|
||||
id: job.id + "_readyToIdealEnd", group: job.id,
|
||||
start: job.readyDateTime,
|
||||
end: job.idealEndDateTime,
|
||||
type: "background",
|
||||
style: "background-color: #8AE23433"
|
||||
});
|
||||
byJobItemDataSet.add({
|
||||
id: job.id + "_idealEndToDue", group: job.id,
|
||||
start: job.idealEndDateTime,
|
||||
end: job.dueDateTime,
|
||||
type: "background",
|
||||
style: "background-color: #FCAF3E33"
|
||||
});
|
||||
|
||||
if (job.line == null || job.startCleaningDateTime == null || job.startProductionDateTime == null || job.endDateTime == null) {
|
||||
unassignedJobsCount++;
|
||||
const durationMinutes = JSJoda.Duration.ofSeconds(job.duration).toMinutes();
|
||||
const unassignedJobElement = $(`<div class="card-body p-2"/>`)
|
||||
.append($(`<h5 class="card-title mb-1"/>`).text(job.name))
|
||||
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`${Math.floor(durationMinutes / 60)} hours ${durationMinutes % 60} mins`))
|
||||
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`Ready: ${JSJoda.LocalDateTime.parse(job.readyDateTime).format(dateTimeFormat)}`))
|
||||
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`Ideal end: ${JSJoda.LocalDateTime.parse(job.idealEndDateTime).format(dateTimeFormat)}`))
|
||||
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`Due: ${JSJoda.LocalDateTime.parse(job.dueDateTime).format(dateTimeFormat)}`));
|
||||
const byJobJobElement = $(`<div/>`)
|
||||
.append($(`<h5 class="card-title mb-1"/>`).text(`Unassigned`));
|
||||
unassignedJobs.append($(`<div class="col"/>`).append($(`<div class="card"/>`).append(unassignedJobElement)));
|
||||
byJobItemDataSet.add({
|
||||
id: job.id, group: job.id,
|
||||
content: byJobJobElement.html(),
|
||||
start: job.readyDateTime, end: JSJoda.LocalDateTime.parse(job.readyDateTime).plus(JSJoda.Duration.ofSeconds(job.duration)).toString(),
|
||||
style: "background-color: #EF292999"
|
||||
});
|
||||
} else {
|
||||
const beforeReady = JSJoda.LocalDateTime.parse(job.startProductionDateTime).isBefore(JSJoda.LocalDateTime.parse(job.readyDateTime));
|
||||
const afterDue = JSJoda.LocalDateTime.parse(job.endDateTime).isAfter(JSJoda.LocalDateTime.parse(job.dueDateTime));
|
||||
const byLineJobElement = $(`<div/>`)
|
||||
.append($(`<p class="card-text"/>`).text(job.name));
|
||||
const byJobJobElement = $(`<div/>`)
|
||||
.append($(`<p class="card-text"/>`).text(job.line.name));
|
||||
if (beforeReady) {
|
||||
byLineJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`Before ready (too early)`));
|
||||
byJobJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`Before ready (too early)`));
|
||||
}
|
||||
if (afterDue) {
|
||||
byLineJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`After due (too late)`));
|
||||
byJobJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`After due (too late)`));
|
||||
}
|
||||
byLineItemDataSet.add({
|
||||
id: job.id + "_cleaning", group: job.line.id,
|
||||
content: "Cleaning",
|
||||
start: job.startCleaningDateTime, end: job.startProductionDateTime,
|
||||
style: "background-color: #FCAF3E99"
|
||||
});
|
||||
byLineItemDataSet.add({
|
||||
id: job.id, group: job.line.id,
|
||||
content: byLineJobElement.html(),
|
||||
start: job.startProductionDateTime, end: job.endDateTime
|
||||
});
|
||||
byJobItemDataSet.add({
|
||||
id: job.id + "_cleaning", group: job.id,
|
||||
content: "Cleaning",
|
||||
start: job.startCleaningDateTime, end: job.startProductionDateTime,
|
||||
style: "background-color: #FCAF3E99"
|
||||
});
|
||||
byJobItemDataSet.add({
|
||||
id: job.id, group: job.id,
|
||||
content: byJobJobElement.html(),
|
||||
start: job.startProductionDateTime, end: job.endDateTime
|
||||
});
|
||||
}
|
||||
});
|
||||
if (unassignedJobsCount === 0) {
|
||||
unassignedJobs.append($(`<p/>`).text(`没有未分配任务`));
|
||||
}
|
||||
const nextDate = JSJoda.LocalDate.parse(schedule.workCalendar.fromDate).plusDays(1);
|
||||
byLineTimeline.setWindow(schedule.workCalendar.fromDate, nextDate.toString());
|
||||
byJobTimeline.setWindow(schedule.workCalendar.fromDate, nextDate.toString());
|
||||
});
|
||||
}
|
||||
|
||||
function solve() {
|
||||
$("#loadingOverlay").show()
|
||||
$.post(baseUrlNew + "schedule/solve", function () {
|
||||
// $.post(baseUrl + "admin-api/aps/order/solve", function () {
|
||||
refreshSolvingButtons(true);
|
||||
}).fail(function (xhr, ajaxOptions, thrownError) {
|
||||
showError("Start solving failed.", xhr);
|
||||
});
|
||||
}
|
||||
|
||||
function analyze() {
|
||||
new bootstrap.Modal("#scoreAnalysisModal").show()
|
||||
const scoreAnalysisModalContent = $("#scoreAnalysisModalContent");
|
||||
scoreAnalysisModalContent.children().remove();
|
||||
if (loadedSchedule.score == null || loadedSchedule.score.indexOf('init') != -1) {
|
||||
scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button.");
|
||||
} else {
|
||||
$('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`);
|
||||
$.put(baseUrl + "admin-api/aps/order/schedule/analyze", function (scoreAnalysis) {
|
||||
let constraints = scoreAnalysis.constraints;
|
||||
constraints.sort((a, b) => {
|
||||
let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score);
|
||||
if (aComponents.hard < 0 && bComponents.hard > 0) return -1;
|
||||
if (aComponents.hard > 0 && bComponents.soft < 0) return 1;
|
||||
if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) {
|
||||
return -1;
|
||||
} else {
|
||||
if (aComponents.medium < 0 && bComponents.medium > 0) return -1;
|
||||
if (aComponents.medium > 0 && bComponents.medium < 0) return 1;
|
||||
if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) {
|
||||
return -1;
|
||||
} else {
|
||||
if (aComponents.soft < 0 && bComponents.soft > 0) return -1;
|
||||
if (aComponents.soft > 0 && bComponents.soft < 0) return 1;
|
||||
|
||||
return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
|
||||
}
|
||||
}
|
||||
});
|
||||
constraints.map((e) => {
|
||||
let components = getScoreComponents(e.weight);
|
||||
e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft');
|
||||
e.weight = components[e.type];
|
||||
let scores = getScoreComponents(e.score);
|
||||
e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft);
|
||||
});
|
||||
scoreAnalysis.constraints = constraints;
|
||||
|
||||
scoreAnalysisModalContent.children().remove();
|
||||
scoreAnalysisModalContent.text("");
|
||||
|
||||
const analysisTable = $(`<table class="table"/>`).css({ textAlign: 'center' });
|
||||
const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
|
||||
.append($(`<th></th>`))
|
||||
.append($(`<th>Constraint</th>`).css({ textAlign: 'left' }))
|
||||
.append($(`<th>Type</th>`))
|
||||
.append($(`<th># Matches</th>`))
|
||||
.append($(`<th>Weight</th>`))
|
||||
.append($(`<th>Score</th>`))
|
||||
.append($(`<th></th>`)));
|
||||
analysisTable.append(analysisTHead);
|
||||
const analysisTBody = $(`<tbody/>`)
|
||||
$.each(scoreAnalysis.constraints, (index, constraintAnalysis) => {
|
||||
let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '<span class="fas fa-exclamation-triangle" style="color: red"></span>' : '';
|
||||
if (!icon) icon = constraintAnalysis.matches.length == 0 ? '<span class="fas fa-check-circle" style="color: green"></span>' : '';
|
||||
|
||||
let row = $(`<tr/>`);
|
||||
row.append($(`<td/>`).html(icon))
|
||||
.append($(`<td/>`).text(constraintAnalysis.name).css({ textAlign: 'left' }))
|
||||
.append($(`<td/>`).text(constraintAnalysis.type))
|
||||
.append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
|
||||
.append($(`<td/>`).text(constraintAnalysis.weight))
|
||||
.append($(`<td/>`).text(constraintAnalysis.implicitScore));
|
||||
analysisTBody.append(row);
|
||||
row.append($(`<td/>`));
|
||||
});
|
||||
analysisTable.append(analysisTBody);
|
||||
scoreAnalysisModalContent.append(analysisTable);
|
||||
}).fail(function (xhr, ajaxOptions, thrownError) {
|
||||
showError("Analyze failed.", xhr);
|
||||
}, "text");
|
||||
}
|
||||
}
|
||||
|
||||
function getScoreComponents(score) {
|
||||
let components = { hard: 0, medium: 0, soft: 0 };
|
||||
|
||||
$.each([...score.matchAll(/(-?[0-9]+)(hard|medium|soft)/g)], (i, parts) => {
|
||||
components[parts[2]] = parseInt(parts[1], 10);
|
||||
});
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
function refreshSolvingButtons(solving) {
|
||||
if (solving) {
|
||||
$("#loadingOverlay").show()
|
||||
$("#solveButton").hide();
|
||||
$("#stopSolvingButton").show();
|
||||
if (autoRefreshIntervalId == null) {
|
||||
autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
|
||||
}
|
||||
} else {
|
||||
$("#loadingOverlay").hide()
|
||||
$("#solveButton").show();
|
||||
$("#stopSolvingButton").hide();
|
||||
if (autoRefreshIntervalId != null) {
|
||||
clearInterval(autoRefreshIntervalId);
|
||||
autoRefreshIntervalId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopSolving() {
|
||||
$("#loadingOverlay").hide()
|
||||
$.post(baseUrlNew + "schedule/stopSolving", function () {
|
||||
// $.post(baseUrl + "admin-api/aps/order/schedule/stopSolving", function () {
|
||||
refreshSolvingButtons(false);
|
||||
refreshSchedule();
|
||||
}).fail(function (xhr, ajaxOptions, thrownError) {
|
||||
showError("Stop solving failed.", xhr);
|
||||
});
|
||||
}
|
||||
7
public/static/bootstrap.min.css
vendored
Normal file
7
public/static/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/static/bootstrap.min.js
vendored
Normal file
7
public/static/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/static/fa-play.svg
Normal file
1
public/static/fa-play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1728628114240" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4279" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0h1024v1024H0z" fill="#000000" opacity=".01" p-id="4280"></path><path d="M520 128C736.496 128 912 303.504 912 520S736.496 912 520 912C303.6 911.76 128.24 736.4 128 520 128 303.504 303.504 128 520 128z m0 60.288c-183.12 0.192-331.52 148.592-331.712 331.712 0 183.2 148.496 331.712 331.712 331.712 183.2 0 331.712-148.512 331.712-331.712S703.2 188.288 520 188.288z m-84.528 176.576l233.776 155.264-233.76 155.024V364.864z" fill="white" p-id="4281"></path></svg>
|
||||
|
After Width: | Height: | Size: 796 B |
1
public/static/fa-stop.svg
Normal file
1
public/static/fa-stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1728628208494" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5253" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" width="200" height="200"><path d="M512 45.38368a450.56 450.56 0 1 0 0 901.12 450.56 450.56 0 0 0 0-901.12z m0 853.6064a403.94752 403.94752 0 1 1-9.78944-807.89504A403.94752 403.94752 0 0 1 512 898.99008z" fill="#000000" p-id="5254"></path><path d="M389.12 373.06368h245.76v245.76H389.12v-245.76z" fill="#000000" p-id="5255"></path></svg>
|
||||
|
After Width: | Height: | Size: 652 B |
114
public/static/index.html
Normal file
114
public/static/index.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
* @Author: zwq
|
||||
* @Date: 2024-10-10 14:20:38
|
||||
* @LastEditors: zwq
|
||||
* @LastEditTime: 2024-10-14 16:01:02
|
||||
* @Description:
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<title>玻璃产线排产示例</title>
|
||||
<!--
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css"
|
||||
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous"> -->
|
||||
|
||||
<link rel="stylesheet" href="vis-timeline-graph2d.min.css"/>
|
||||
<link rel="stylesheet" href="bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="all.css"/>
|
||||
<link rel="stylesheet" href="timefold-webui.css" />
|
||||
<style>
|
||||
.vis-time-axis .vis-grid.vis-saturday,
|
||||
.vis-time-axis .vis-grid.vis-sunday {
|
||||
background: #D3D7CFFF;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color: white;">
|
||||
<div class="container-fluid">
|
||||
<div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite" aria-atomic="true">
|
||||
<div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
|
||||
</div>
|
||||
<h1>玻璃产线排产</h1>
|
||||
<p>为您的玻璃生产线制定最佳计划。</p>
|
||||
|
||||
<div class="mb-2">
|
||||
<button id="refreshButton" type="button" class="btn btn-secondary">
|
||||
<span class="fas fa-refresh"></span> 刷新
|
||||
</button>
|
||||
<button id="solveButton" type="button" class="btn btn-success">
|
||||
<img src="fa-play.svg" alt="SVG Image" width="18" height="18"> 计算
|
||||
</button>
|
||||
<button id="stopSolvingButton" type="button" class="btn btn-danger">
|
||||
<img src="fa-stop.svg" alt="SVG Image" width="18" height="18"> 停止计算
|
||||
</button>
|
||||
<!-- <span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
|
||||
<button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
|
||||
<span class="fas fa-question"></span>
|
||||
</button> -->
|
||||
|
||||
<div class="float-end">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="byLineTab" data-bs-toggle="tab" data-bs-target="#byLinePanel" type="button" role="tab" aria-controls="byLinePanel" aria-selected="true">产线视图</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="byJobTab" data-bs-toggle="tab" data-bs-target="#byJobPanel" type="button" role="tab" aria-controls="byJobPanel" aria-selected="false">任务视图</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 tab-content">
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="card-container">
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
</div>
|
||||
<div class="loading-text">计算中...</div>
|
||||
</div>
|
||||
<div class="tab-pane fade show active" id="byLinePanel" role="tabpanel" aria-labelledby="byLineTab">
|
||||
<div id="lineVisualization"></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="byJobPanel" role="tabpanel" aria-labelledby="byJobTab">
|
||||
<div id="jobVisualization"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>未分配任务</h2>
|
||||
<div id="unassignedJobs" class="row row-cols-3 g-3 mb-4"></div>
|
||||
</div>
|
||||
<div class="modal fadebd-example-modal-lg" id="scoreAnalysisModal" tabindex="-1"
|
||||
aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="scoreAnalysisModalLabel">Score analysis <span
|
||||
id="scoreAnalysisScoreLabel"></span></h1>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="scoreAnalysisModalContent">
|
||||
<!-- Filled in by app.js -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="jquery-3.6.4.min.js"></script>
|
||||
<script src="moment-with-locales.js"></script>
|
||||
<script src="bootstrap.min.js"></script>
|
||||
<script src="js-joda.min.js"></script>
|
||||
<script src="vis-timeline-graph2d.min.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js"
|
||||
integrity="sha256-Jy2+UO7rZ2Dgik50z3XrrNpnc5+2PAx9MhL2CicodME=" crossorigin="anonymous"></script> -->
|
||||
<script src="timefold-webui.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2
public/static/jquery-3.6.4.min.js
vendored
Normal file
2
public/static/jquery-3.6.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
public/static/js-joda.min.js
vendored
Normal file
5
public/static/js-joda.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18472
public/static/moment-with-locales.js
Normal file
18472
public/static/moment-with-locales.js
Normal file
File diff suppressed because it is too large
Load Diff
60
public/static/timefold-webui.css
Normal file
60
public/static/timefold-webui.css
Normal file
@@ -0,0 +1,60 @@
|
||||
:root {
|
||||
/* Keep in sync with .navbar height on a large screen. */
|
||||
--ts-navbar-height: 109px;
|
||||
|
||||
--ts-violet-1-rgb: #3E00FF;
|
||||
--ts-violet-2-rgb: #3423A6;
|
||||
--ts-violet-3-rgb: #2E1760;
|
||||
--ts-violet-4-rgb: #200F4F;
|
||||
--ts-violet-5-rgb: #000000; /* TODO FIXME */
|
||||
--ts-violet-dark-1-rgb: #b6adfd;
|
||||
--ts-violet-dark-2-rgb: #c1bbfd;
|
||||
--ts-gray-rgb: #666666;
|
||||
--ts-white-rgb: #FFFFFF;
|
||||
--ts-light-rgb: #F2F2F2;
|
||||
--ts-gray-border: #c5c5c5;
|
||||
|
||||
--tf-light-rgb-transparent: rgb(242,242,242,0.5); /* #F2F2F2 = rgb(242,242,242) */
|
||||
--bs-body-bg: var(--ts-light-rgb); /* link to html bg */
|
||||
--bs-link-color: var(--ts-violet-1-rgb);
|
||||
--bs-link-hover-color: var(--ts-violet-2-rgb);
|
||||
|
||||
--bs-navbar-color: var(--ts-white-rgb);
|
||||
--bs-navbar-hover-color: var(--ts-white-rgb);
|
||||
--bs-nav-link-font-size: 18px;
|
||||
--bs-nav-link-font-weight: 400;
|
||||
--bs-nav-link-color: var(--ts-white-rgb);
|
||||
--ts-nav-link-hover-border-color: var(--ts-violet-1-rgb);
|
||||
}
|
||||
.btn {
|
||||
--bs-btn-border-radius: 1.5rem;
|
||||
}
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--ts-violet-1-rgb);
|
||||
--bs-btn-border-color: var(--ts-violet-1-rgb);
|
||||
--bs-btn-hover-bg: var(--ts-violet-2-rgb);
|
||||
--bs-btn-hover-border-color: var(--ts-violet-2-rgb);
|
||||
--bs-btn-active-bg: var(--ts-violet-2-rgb);
|
||||
--bs-btn-active-border-bg: var(--ts-violet-2-rgb);
|
||||
--bs-btn-disabled-bg: var(--ts-violet-1-rgb);
|
||||
--bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--ts-violet-1-rgb);
|
||||
--bs-btn-border-color: var(--ts-violet-1-rgb);
|
||||
--bs-btn-hover-bg: var(--ts-violet-1-rgb);
|
||||
--bs-btn-hover-border-color: var(--ts-violet-1-rgb);
|
||||
--bs-btn-active-bg: var(--ts-violet-1-rgb);
|
||||
--bs-btn-active-border-color: var(--ts-violet-1-rgb);
|
||||
--bs-btn-disabled-color: var(--ts-violet-1-rgb);
|
||||
--bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
|
||||
}
|
||||
.navbar-dark {
|
||||
--bs-link-color: var(--ts-violet-dark-1-rgb);
|
||||
--bs-link-hover-color: var(--ts-violet-dark-2-rgb);
|
||||
--bs-navbar-color: var(--ts-white-rgb);
|
||||
--bs-navbar-hover-color: var(--ts-white-rgb);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--ts-violet-1-rgb);
|
||||
}
|
||||
142
public/static/timefold-webui.js
Normal file
142
public/static/timefold-webui.js
Normal file
@@ -0,0 +1,142 @@
|
||||
function replaceTimefoldAutoHeaderFooter() {
|
||||
const timefoldHeader = $("header#timefold-auto-header");
|
||||
if (timefoldHeader != null) {
|
||||
timefoldHeader.addClass("bg-black")
|
||||
timefoldHeader.append(
|
||||
$(`<div class="container-fluid">
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-dark shadow mb-3">
|
||||
<a class="navbar-brand" href="https://timefold.ai">
|
||||
<img src="webjars/timefold/img/timefold-logo-horizontal-negative.svg" alt="Timefold logo" width="200">
|
||||
</a>
|
||||
</nav>
|
||||
</div>`));
|
||||
}
|
||||
const timefoldFooter = $("footer#timefold-auto-footer");
|
||||
if (timefoldFooter != null) {
|
||||
timefoldFooter.append(
|
||||
$(`<footer class="bg-black text-white-50">
|
||||
<div class="container">
|
||||
<div class="hstack gap-3 p-4">
|
||||
<div class="ms-auto"><a class="text-white" href="https://timefold.ai">Timefold</a></div>
|
||||
<div class="vr"></div>
|
||||
<div><a class="text-white" href="https://timefold.ai/docs">Documentation</a></div>
|
||||
<div class="vr"></div>
|
||||
<div><a class="text-white" href="https://github.com/TimefoldAI/timefold-quickstarts">Code</a></div>
|
||||
<div class="vr"></div>
|
||||
<div class="me-auto"><a class="text-white" href="https://timefold.ai/product/support/">Support</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="applicationInfo" class="container text-center"></div>
|
||||
</footer>`));
|
||||
|
||||
applicationInfo();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function showSimpleError(title) {
|
||||
const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
|
||||
.append($(`<div class="toast-header bg-danger">
|
||||
<strong class="me-auto text-dark">Error</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>`))
|
||||
.append($(`<div class="toast-body"/>`)
|
||||
.append($(`<p/>`).text(title))
|
||||
);
|
||||
$("#notificationPanel").append(notification);
|
||||
notification.toast({delay: 30000});
|
||||
notification.toast('show');
|
||||
}
|
||||
|
||||
function showError(title, xhr) {
|
||||
var serverErrorMessage = !xhr.responseJSON ? `${xhr.status}: ${xhr.statusText}` : xhr.responseJSON.message;
|
||||
var serverErrorCode = !xhr.responseJSON ? `unknown` : xhr.responseJSON.code;
|
||||
var serverErrorId = !xhr.responseJSON ? `----` : xhr.responseJSON.id;
|
||||
var serverErrorDetails = !xhr.responseJSON ? `no details provided` : xhr.responseJSON.details;
|
||||
|
||||
if (xhr.responseJSON && !serverErrorMessage) {
|
||||
serverErrorMessage = JSON.stringify(xhr.responseJSON);
|
||||
serverErrorCode = xhr.statusText + '(' + xhr.status + ')';
|
||||
serverErrorId = `----`;
|
||||
}
|
||||
|
||||
console.error(title + "\n" + serverErrorMessage + " : " + serverErrorDetails);
|
||||
const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
|
||||
.append($(`<div class="toast-header bg-danger">
|
||||
<strong class="me-auto text-dark">Error</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>`))
|
||||
.append($(`<div class="toast-body"/>`)
|
||||
.append($(`<p/>`).text(title))
|
||||
.append($(`<pre/>`)
|
||||
.append($(`<code/>`).text(serverErrorMessage + "\n\nCode: " + serverErrorCode + "\nError id: " + serverErrorId))
|
||||
)
|
||||
);
|
||||
$("#notificationPanel").append(notification);
|
||||
notification.toast({delay: 30000});
|
||||
notification.toast('show');
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Application info
|
||||
// ****************************************************************************
|
||||
|
||||
function applicationInfo() {
|
||||
$.getJSON("info", function (info) {
|
||||
$("#applicationInfo").append("<small>" + info.application + " (version: " + info.version + ", built at: " + info.built + ")</small>");
|
||||
}).fail(function (xhr, ajaxOptions, thrownError) {
|
||||
console.warn("Unable to collect application information");
|
||||
});
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// TangoColorFactory
|
||||
// ****************************************************************************
|
||||
|
||||
const SEQUENCE_1 = [0x8AE234, 0xFCE94F, 0x729FCF, 0xE9B96E, 0xAD7FA8];
|
||||
const SEQUENCE_2 = [0x73D216, 0xEDD400, 0x3465A4, 0xC17D11, 0x75507B];
|
||||
|
||||
var colorMap = new Map;
|
||||
var nextColorCount = 0;
|
||||
|
||||
function pickColor(object) {
|
||||
let color = colorMap[object];
|
||||
if (color !== undefined) {
|
||||
return color;
|
||||
}
|
||||
color = nextColor();
|
||||
colorMap[object] = color;
|
||||
return color;
|
||||
}
|
||||
|
||||
function nextColor() {
|
||||
let color;
|
||||
let colorIndex = nextColorCount % SEQUENCE_1.length;
|
||||
let shadeIndex = Math.floor(nextColorCount / SEQUENCE_1.length);
|
||||
if (shadeIndex === 0) {
|
||||
color = SEQUENCE_1[colorIndex];
|
||||
} else if (shadeIndex === 1) {
|
||||
color = SEQUENCE_2[colorIndex];
|
||||
} else {
|
||||
shadeIndex -= 3;
|
||||
let floorColor = SEQUENCE_2[colorIndex];
|
||||
let ceilColor = SEQUENCE_1[colorIndex];
|
||||
let base = Math.floor((shadeIndex / 2) + 1);
|
||||
let divisor = 2;
|
||||
while (base >= divisor) {
|
||||
divisor *= 2;
|
||||
}
|
||||
base = (base * 2) - divisor + 1;
|
||||
let shadePercentage = base / divisor;
|
||||
color = buildPercentageColor(floorColor, ceilColor, shadePercentage);
|
||||
}
|
||||
nextColorCount++;
|
||||
return "#" + color.toString(16);
|
||||
}
|
||||
|
||||
function buildPercentageColor(floorColor, ceilColor, shadePercentage) {
|
||||
let red = (floorColor & 0xFF0000) + Math.floor(shadePercentage * ((ceilColor & 0xFF0000) - (floorColor & 0xFF0000))) & 0xFF0000;
|
||||
let green = (floorColor & 0x00FF00) + Math.floor(shadePercentage * ((ceilColor & 0x00FF00) - (floorColor & 0x00FF00))) & 0x00FF00;
|
||||
let blue = (floorColor & 0x0000FF) + Math.floor(shadePercentage * ((ceilColor & 0x0000FF) - (floorColor & 0x0000FF))) & 0x0000FF;
|
||||
return red | green | blue;
|
||||
}
|
||||
2
public/static/vis-timeline-graph2d.min.css
vendored
Normal file
2
public/static/vis-timeline-graph2d.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
54
public/static/vis-timeline-graph2d.min.js
vendored
Normal file
54
public/static/vis-timeline-graph2d.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user