在Layui中实现甘特图; 项目框架是C# MVC,前端使用的是layui 寻求一款免费好用的甘特图组件。数日搜寻后无果,最后向Vue妥协 最后把layui和vue拼凑到一个页面,为啥不统一用vue呢,领导想要页面风格统一,只允许甘特图部分使用vue............
引入Vue 参考https://blog.csdn.net/s_156/article/details/139409387 下面是我完整实现的前端代码
源码
<!DOCTYPE html>
<html charset="utf-8">
<head>
<meta charset="utf-8" />
<title>项目人力分布</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
@Html.Style("~/Assets/layuiadmin/layui/css/layui.css")
@Html.Style("~/Assets/layuiadmin/style/admin.css")
@Html.Style("~/Assets/css/layui-ext.css")
@Html.Style("~/Assets/controls/formSelects-master/formSelects-v4.css")
<script src="~/Assets/js/vue/vue.js"></script>
<script src="~/Assets/js/vue/dayjs.js"></script>
<script src="~/Assets/js/vue/pagination.js"></script>
<script src="~/Assets/js/vue/GanttElastic.umd.js"></script>
<script src="~/Assets/js/vue/Header.umd.js"></script>
<style>
.gantt-elastic__chart-row-task-wrapper:hover .gantt-elastic__chart-row-text-wrapper {
display: block !important;
}
.gantt-elastic__chart-row-project-wrapper:hover .gantt-elastic__chart-row-text-wrapper {
display: block !important;
}
.gantt-elastic__chart-row-milestone-wrapper:hover .gantt-elastic__chart-row-text-wrapper {
display: block !important;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.pagination button {
padding: 5px 10px;
margin: 0 5px;
color: black;
border: none;
cursor: pointer;
}
.pagination button.disabled {
cursor: not-allowed;
}
select {
padding: 5px 10px;
color: #000;
border: none;
cursor: pointer;
}
.pagination input[type="number"]::-webkit-outer-spin-button,
.pagination input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
@*
.gantt-elastic {
height: calc(100vh - 125px) !important;
overflow-y: scroll;
}
.gantt-elastic__task-list-items{
height: calc(100vh - 125px) !important;
} *@
</style>
</head>
<body>
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto"
lay-filter="filter-form-query" id="header">
<input type="hidden" name="allSales" id="ipt-allSales" />
<input type="hidden" name="leader" id="ipt-leader" />
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">模糊查询</label>
<div class="layui-input-inline">
<input type="text" name="fuzzyContent" placeholder="请输入" autocomplete="off"
class="layui-input" id="fuzzyContent">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">年份</label>
<div class="layui-input-block">
<input type="text" class="layui-input" id="ID-laydate-type-year" name="year"
placeholder="yyyy" lay-verify="year">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">项目类型</label>
<div class="layui-input-block" style="width: 250px;">
<select name="projectType" id="projectType" xm-select="xm-sel-type"
xm-select-skin="default">
<option value="">请选择项目类型</option>
<option value="1">锂电项目</option>
<option value="2">电机项目</option>
<option value="3">研发项目</option>
<option value="4">测试项目</option>
@* <option value="5">贸易项目</option> *@
<option value="6">一般项目</option>
@* <option value="7">售后项目</option> *@
<option value="8">其他项目</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">工程状态</label>
<div class="layui-input-inline">
<select id="states" name="states" xm-select="xm-sel-states"
xm-select-skin="default">
<option value=''>请选择项目类型</option>
<option value='1'>设计中</option>
<option value='2'>设计完结</option>
<option value='3'>已发货</option>
<option value='4'>已竣工</option>
<option value='5'>已验收</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn layuiadmin-btn-admin" lay-submit id="btn-query"
lay-filter="filter-btn-query">
<i class="layui-icon layui-icon-search layuiadmin-button-btn"></i>
</button>
</div>
</div>
</div>
<div class="layui-card-body" id="app">
<div style="height: calc(100vh - 128px);">
<div v-show="!destroy" style="height: 100%;">
<gantt-elastic :tasks="tasks" :options="options" :dynamic-style="dynamicStyle" style="height: 90%;">
<gantt-header slot="header"></gantt-header>
</gantt-elastic>
<pagin-ation v-if="pageshow" class="pagination" ref="pagination" :sum="sumRows" style="height: 10%;"></pagin-ation>
</div>
<div v-show="destroy">
<p style="text-align: center; font-size: 24px; color: rgba(0, 0, 0, 0.5); margin-top: 100px;">无数据</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@Html.Script("~/Assets/layuiadmin/layui/layui.js")
@Html.Script("~/Assets/js/ext/ext-function.js")
@Html.Script("~/Assets/controls/formSelects-master/formSelects-v4.min.js")
<script>
var $ = layui.$;
// just helper to get current dates
function getDate(hours) {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth();
const currentDay = currentDate.getDate();
const timeStamp = new Date(currentYear, currentMonth, currentDay, 0, 0, 0).getTime();
return new Date(timeStamp + hours * 60 * 60 * 1000).getTime();
}
let stateVaule='';
let typeVaule='';
let editRights=false;
let tasks1 = [
{
id: 1,
start: 0,
duration: 0,//毫秒
},
{
id: 2,
parentId: 1,
start: 0,
duration: 0,
type: 'task',
}
];
let options = {
// maxRows: 100,
maxHeight: 600,
title: {
label: 'Your project title as html (link or whatever...)',
html: false,
},
row: {
height: 15,
},
times: {
timeZoom: 22,
},
calendar: {
hour: {
display: false,
},
},
chart: {
progress: {
bar: false,
},
expander: {
display: true,
},
},
locale: {
weekdays: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
},
taskList: {
expander: {
straight: false,
},
columns: [
{
id: 1,
label: '序号',
value: 'numb',
width: 50,
},
{
id: 2,
label: '项目编号',
value: 'code',
width: 90,
expander: true,
},
{
id: 3,
label: '项目名称',
value: 'name',
width: 130,
html: true,
},
{
id: 4,
label: '计划/实际',
value: 'stage',
// value (task) => dayjs(task.start).format('YYYY-MM-DD'),
width: 75,
},
{
id: 5,
label: '开始时间',
value: 'beginDate',
value: (task) => dayjs(task.beginDate).format('YYYY-MM-DD'),
width: 80,
},
{
id: 6,
label: '结束时间',
value: 'endDate',
//value: (task) => dayjs(task.endDate).format('YYYY-MM-DD'),
width: 80,
},
{
id: 7,
label: '通知采购',
value: 'notice',
width: 80,
},
{
id: 8,
label: '天数',
value: 'days',
width: 55,
}
],
},
};
// create instance
let btn = document.getElementById("btn-query")
document.onkeydown = function (e) {
var e = event.srcElement;
if (event.keyCode == 13) {
document.getElementById("btn-query").click();
return false;
}
};
const app = new Vue({
components: {
'pagin-ation': window.PaginationComponentForHTML,
'gantt-header': Header,
'gantt-elastic': GanttElastic,
'gantt-footer': {
template: `<span>this is a footer</span>`,
},
},
data: {
tasks: tasks1,
options,
dynamicStyle: {
'task-list-header-label': {
'font-weight': 'bold',
},
},
sumRows: 10,
destroy: false,
pageshow:false,
},
mounted() {
initStates();
options.times.timeZoom = 1;
this.load(1,50);
},
methods: {
getCurrentPage(val) {
console.log(val);
this.load(val.currentPage, val.pageSize);
},
load(page,limit) {
let allPro= document.getElementById("ipt-allSales").value;
let leader= document.getElementById("ipt-leader").value;
let projectType =typeVaule;
let year = document.getElementById("ID-laydate-type-year").value;
let states = stateVaule;
let fuzzyContent = document.getElementById("fuzzyContent").value;
const params = new URLSearchParams({
allSales:allPro,leader:leader,fuzzyContent: fuzzyContent,
projectTypeId: projectType, year: year, states: states,page:page,limit:limit
}).toString();
fetch(`/Project/GetProjectGantt?${params}`, {
methods: "get",
}).then((r) => r.json()).then((res) => {
const maxValue = Math.max(...res.Data.map(item => item.days));
this.options.times.timeZoom = calculateTimeZoom(maxValue)
this.pageshow=true;
console.log(res);
if (res.Data.length > 0) {
this.destroy=false;
this.tasks = res.Data;
this.sumRows = res.Count;
} else {
this.destroy=true;
@* var aa = [];
this.tasks = tasks1; *@
this.sumRows=0;
}
})
}
},
});
btn.onclick = function () {
app.load(1,50)
}
// gantt state which will be updated in realtime
let ganttState, ganttInstance;
// listen to 'gantt-elastic.ready' or 'gantt-elastic.mounted' event
// to get the gantt state for real time modification
app.$on('gantt-elastic-ready', (ganttElasticInstance) => {
ganttInstance = ganttElasticInstance;
ganttInstance.$on('tasks-changed', (tasks) => {
// console.log('1');
app.tasks = tasks;
});
ganttInstance.$on('options-changed', (options) => {
options.maxHeight = getMaxHeight()
app.options = options;
});
ganttInstance.$on('dynamic-style-changed', (style) => {
// console.log('3');
app.dynamicStyle = style;
});
ganttInstance.$on('chart-task-mouseenter', ({ data, event }) => {
// console.log('4');
console.log('task mouse enter', { data, event });
});
ganttInstance.$on('updated', () => {
//console.log('gantt view was updated');
// console.log('5');
});
ganttInstance.$on('destroyed', () => {
//console.log('gantt was destroyed');
});
ganttInstance.$on('times-timeZoom-updated', () => {
console.log('time zoom changed');
});
ganttInstance.$on('taskList-task-click', ({ event, data, column }) => {
dialogProject(data);
});
});
function getMaxHeight() {
return window.innerHeight - 165 - document.querySelector("#header").offsetHeight;
}
window.addEventListener('resize', () => {
getMaxHeight()
})
// mount gantt to DOM
var $ = layui.$,
form = layui.form,
table = layui.table,
laydate = layui.laydate,
formSelects = layui.formSelects;
layer = layui.layer;
formSelects.btns('xm-sel-type', ['select', 'remove'], { show: '' });
formSelects.btns('xm-sel-states', ['select', 'remove'], { show: '' });
var defaultDate = new Date().getFullYear();
laydate.render({
elem: '#ID-laydate-type-year',
type: 'year',
value: defaultDate, // 设置默认值为当前时间
format: 'yyyy', // 年份格式
change: function (value, date, endDate) {
}
});
layui.formSelects.on('xm-sel-states', function (id, vals, val, isAdd, isDisabled) {
let arr=[];
vals.forEach((item)=>{
arr.push(item.value)
})
stateVaule=arr;
}, true);
layui.formSelects.on('xm-sel-type', function (id, vals, val, isAdd, isDisabled) {
let arr=[];
vals.forEach((item)=>{
arr.push(item.value)
})
typeVaule=arr;
}, true);
app.$mount('#app');
function dialogProject(entity) {
if(editRights){
layer.open({
type: 2, //1:页面层; 2:iframe;
title: '编辑项目',
content: '/Project/ProjectEditForm',
maxmin: false,
resize: false,
area: ['750px', '750px'],
btn: ['确定', '取消'],
skin: 'layer-ext-wit',
success: function (layero, index) {
//得到iframe页的窗口对象
var iframeWin = window[layero.find('iframe')[0]['name']];
//执行iframe页的全局方法:iframeWin.method();
if (entity != null) {
iframeWin.initData(entity.tableId);
}
},
yes: function (index, layero) {
var iframeWin = window['layui-layer-iframe' + index],
submit = layero.find('iframe').contents().find('#btn-submit');
//监听提交
iframeWin.layui.form.on('submit(filter-btn-submit)', function (data) {
var field = data.field; //获取提交的字段
if (field.PlanStageEndDate != '') {
if (field.PlanStageBeginDate > field.PlanStageEndDate) {
layer.msg('计划设计开始时间不能大于计划结束时间');
return;
}
}
if (field.DesStageEndDate != '') {
if (field.DesStageBeginDate > field.DesStageEndDate) {
layer.msg('实际设计开始时间不能大于实际结束时间');
return;
}
if (field.ProjectState < 2) {
layer.msg('有设计结束时间,状态应该处于设计完结或者之后的状态');
return;
}
}
layer.load(1);
$.ajax({
type: 'post',
url: '/Project/SaveProject',
data: field,
dataType: 'json',
success: function (response) {
if (response.Result == 0) {
app.load();
layer.close(index);
layer.msg(entity == null ? '添加成功' : '编辑成功');
}
else if (response.Result == -10) {
layer.msg(response.Msg);
}
else {
layer.alert(response.Msg, { title: '错误信息', skin: 'layer-ext-danger' });
}
},
error: function (response) {
console.log(response.responseText);
},
complete: function (response) {
layer.closeAll('loading');
}
});
});
submit.trigger('click');
}
});
}
}
let test = document.getElementsByClassName("gantt-elastic__header-title")[0]
var getDate = dayjs().format('YYYY-MM-DD');
test.innerHTML = getDate + ' 在设计项目';
function initStates() {
@* var states = JSONParse('@Html.Raw(ViewBag.States)');
for (let i = 0; i < states.length; i++) {
$('#sel-states').append('<option value=' + states[i].Code + '>' + states[i].Name + '</option>');
};
form.render('select', 'filter-form-query');
formSelects.render('xm-sel-states'); *@
if ('@Html.Raw(ViewBag.allSales)' == 'True') {
$('#ipt-allSales').val(1);
} else {
$('#ipt-allSales').val(0);
}
var dd = '@Html.Raw(ViewBag.LeaderList)';
$('#ipt-leader').val(dd);
if('@Html.Raw(ViewBag.edit)' == 'True'){
editRights=true;
}
form.render('select', 'filter-form-data');
};
function calculateTimeZoom(days) {
const data = [
[0, 6],
[2, 10],
[5, 15],
[8, 17.5],
[15, 19.5],
[29, 21],
[34, 21],
[36, 21],
[41, 21.5],
[48, 21.5],
[54, 22],
[65, 22],
[68, 22]
];
let lowerIndex = 0;
let upperIndex = data.length - 1;
while (lowerIndex < upperIndex - 1) {
const midIndex = Math.floor((lowerIndex + upperIndex) / 2);
if (data[midIndex][0] < days) {
lowerIndex = midIndex;
} else {
upperIndex = midIndex;
}
}
const lowerPair = data[lowerIndex];
const upperPair = data[upperIndex];
if (days >= upperPair[0]) {
return upperPair[1];
}
const ratio = (days - lowerPair[0]) / (upperPair[0] - lowerPair[0]);
return lowerPair[1] + ratio * (upperPair[1] - lowerPair[1]);
}
</script>
</body>
</html>
里面添加了分页的逻辑,对应的JS代码:
分页组件
const PaginationComponent = {
name: 'Pagination',
template: `<div ref="pagination">这里是分页组件的模板内容</div>`,
props: {
sum: {
type: Number,
required: true
}
},
data() {
return {
totalPages: 0,
currentPage: 1,
pageSizeOptions: [10, 20, 50, 100],
pageSize: 50
};
},
methods: {
updatePagination() {
const pagination = this.$refs.pagination;
pagination.innerHTML = '';
// 首页按钮
const firstButton = document.createElement('button');
firstButton.textContent = '首页';
firstButton.addEventListener('click', () => {
this.currentPage = 1;
this.updatePagination();
});
pagination.appendChild(firstButton);
if (this.currentPage > 1) {
const prevButton = document.createElement('button');
prevButton.textContent = '上一页';
prevButton.addEventListener('click', () => {
this.currentPage--;
this.updatePagination();
});
pagination.appendChild(prevButton);
}
// 处理页码显示逻辑
if (this.totalPages <= 10) {
for (let i = 1; i <= this.totalPages; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
if (i === this.currentPage) {
pageButton.style.color = 'white';
pageButton.style.background = '#009688';
} else {
pageButton.style.color = '#000';
}
pageButton.addEventListener('click', () => {
this.currentPage = i;
this.updatePagination();
});
pagination.appendChild(pageButton);
}
} else {
if (this.currentPage <= 4) {
for (let i = 1; i <= 5; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
if (i === this.currentPage) {
pageButton.style.color = 'white';
pageButton.style.background = '#009688';
} else {
pageButton.style.color = '#000';
}
pageButton.addEventListener('click', () => {
this.currentPage = i;
this.updatePagination();
});
pagination.appendChild(pageButton);
}
pagination.appendChild(document.createTextNode('...'));
pagination.appendChild(document.createElement('button')).textContent = this.totalPages;
} else if (this.currentPage >= this.totalPages - 3) {
pagination.appendChild(document.createElement('button')).textContent = 1;
pagination.appendChild(document.createTextNode('...'));
for (let i = this.totalPages - 4; i <= this.totalPages; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
if (i === this.currentPage) {
pageButton.style.color = 'white';
pageButton.style.background = '#009688';
} else {
pageButton.style.color = '#000';
}
pageButton.addEventListener('click', () => {
this.currentPage = i;
this.updatePagination();
});
pagination.appendChild(pageButton);
}
} else {
pagination.appendChild(document.createElement('button')).textContent = 1;
pagination.appendChild(document.createTextNode('...'));
const middleButton1 = document.createElement('button');
middleButton1.textContent = this.currentPage - 1;
if (this.currentPage - 1 === this.currentPage) {
middleButton1.style.color = '#000';
} else {
middleButton1.style.color = '#000';
}
middleButton1.addEventListener('click', () => {
this.currentPage = this.currentPage - 1;
this.updatePagination();
});
pagination.appendChild(middleButton1);
const middleButton2 = document.createElement('button');
middleButton2.textContent = this.currentPage;
middleButton2.style.color = 'white';
middleButton2.style.background = '#009688';
middleButton2.addEventListener('click', () => {
this.currentPage = this.currentPage;
this.updatePagination();
});
pagination.appendChild(middleButton2);
const middleButton3 = document.createElement('button');
middleButton3.textContent = this.currentPage + 1;
if (this.currentPage + 1 === this.currentPage) {
middleButton3.style.color = '#000';
} else {
middleButton3.style.color = '#000';
}
middleButton3.addEventListener('click', () => {
this.currentPage = this.currentPage + 1;
this.updatePagination();
});
pagination.appendChild(middleButton3);
pagination.appendChild(document.createTextNode('...'));
pagination.appendChild(document.createElement('button')).textContent = this.totalPages;
}
}
if (this.currentPage < this.totalPages) {
const nextButton = document.createElement('button');
nextButton.textContent = '下一页';
nextButton.addEventListener('click', () => {
this.currentPage++;
this.updatePagination();
});
pagination.appendChild(nextButton);
}
// 尾页按钮
const lastButton = document.createElement('button');
lastButton.textContent = '尾页';
lastButton.addEventListener('click', () => {
this.currentPage = this.totalPages;
this.updatePagination();
});
pagination.appendChild(lastButton);
const sum = document.createElement('div');
sum.textContent = `共 ${this.sum} 条`;
pagination.appendChild(sum);
// 每页条数下拉选择框
const pageSizeSelect = document.createElement('select');
this.pageSizeOptions.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option;
optionElement.textContent = `每页 ${option} 条`;
if (option === this.pageSize) {
optionElement.selected = true;
}
pageSizeSelect.appendChild(optionElement);
});
pageSizeSelect.addEventListener('change', () => {
this.pageSize = parseInt(pageSizeSelect.value);
// 重新计算总页数
this.totalPages = Math.ceil(this.sum / this.pageSize);
this.currentPage = 1;
this.updatePagination();
});
pagination.appendChild(pageSizeSelect);
// 输入页码的输入框和跳转按钮
const inputContainer = document.createElement('div');
const pageInput = document.createElement('input');
pageInput.type = 'number';
pageInput.min = 1;
pageInput.max = this.totalPages;
pageInput.value = this.currentPage;
pageInput.style.padding = "3px";
const goButton = document.createElement('button');
goButton.textContent = '跳转';
goButton.addEventListener('click', () => {
const inputPage = parseInt(pageInput.value);
if (!isNaN(inputPage) && inputPage >= 1 && inputPage <= this.totalPages) {
this.currentPage = inputPage;
this.updatePagination();
}
});
inputContainer.appendChild(pageInput);
inputContainer.appendChild(goButton);
pagination.appendChild(inputContainer);
inputContainer.onkeyup = (e) => {
if (e.keyCode === 13) {
this.currentPage = pageInput.value;
this.updatePagination();
}
};
}
},
mounted() {
this.totalPages = Math.ceil(this.sum / this.pageSize);
this.updatePagination();
document.addEventListener('click', function (event) {
if (event.target.tagName === 'BUTTON' && !event.target.classList.contains('disabled')) {
const pageNumber = parseInt(event.target.textContent);
if (!isNaN(pageNumber)) {
this.currentPage = pageNumber;
this.updatePagination();
}
}
}.bind(this));
},
watch: {
currentPage(newVal, oldVal) {
let obj = {
currentPage: newVal,
pageSize: this.pageSize
}
this.$parent.getCurrentPage(obj)
this.totalPages = Math.ceil(this.sum / this.pageSize);
this.updatePagination()
},
pageSize(newVal, oldVal) {
let obj = {
currentPage: this.currentPage,
pageSize: newVal
}
this.$parent.getCurrentPage(obj)
this.totalPages = Math.ceil(this.sum / this.pageSize);
this.updatePagination()
},
sum(newVal, oldVal) {
this.totalPages = Math.ceil(this.sum / this.pageSize);
this.updatePagination()
}
}
};
window.PaginationComponentForHTML = PaginationComponent;
浙公网安备 33010602011771号