甘特图组件,有点改炸了
This commit is contained in:
43
components/Gantt/core/DataManager.js
Normal file
43
components/Gantt/core/DataManager.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// DataManager:任务数据管理器,负责任务的增删改查、分组、校验和变更通知
|
||||||
|
export default class DataManager {
|
||||||
|
constructor(tasks = [], dimensions = []) {
|
||||||
|
this.tasks = tasks;
|
||||||
|
this.dimensions = dimensions;
|
||||||
|
this.listeners = [];
|
||||||
|
}
|
||||||
|
// 新增任务
|
||||||
|
addTask(task) {
|
||||||
|
this.tasks.push(task);
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
// 更新任务
|
||||||
|
updateTask(id, data) {
|
||||||
|
const idx = this.tasks.findIndex(t => t.id === id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.tasks[idx] = { ...this.tasks[idx], ...data };
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除任务
|
||||||
|
removeTask(id) {
|
||||||
|
this.tasks = this.tasks.filter(t => t.id !== id);
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
// 按维度分组
|
||||||
|
groupByDimension(dim) {
|
||||||
|
const groups = {};
|
||||||
|
this.tasks.forEach(task => {
|
||||||
|
const key = task.dimensions && task.dimensions[dim] ? task.dimensions[dim].id : '未分组';
|
||||||
|
if (!groups[key]) groups[key] = { id: key, name: task.dimensions && task.dimensions[dim] ? task.dimensions[dim].name : '未分组', tasks: [] };
|
||||||
|
groups[key].tasks.push(task);
|
||||||
|
});
|
||||||
|
return Object.values(groups);
|
||||||
|
}
|
||||||
|
// 变更监听
|
||||||
|
onChange(cb) {
|
||||||
|
this.listeners.push(cb);
|
||||||
|
}
|
||||||
|
notify() {
|
||||||
|
this.listeners.forEach(cb => cb(this.tasks));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
components/Gantt/core/Interaction.js
Normal file
27
components/Gantt/core/Interaction.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Interaction:交互控制器,处理用户交互逻辑,输出标准化事件
|
||||||
|
export default class Interaction {
|
||||||
|
constructor(dataManager, timeCalculator) {
|
||||||
|
this.dataManager = dataManager;
|
||||||
|
this.timeCalculator = timeCalculator;
|
||||||
|
}
|
||||||
|
// 处理拖拽开始
|
||||||
|
handleDragStart(taskId) {
|
||||||
|
// 可扩展:记录初始状态
|
||||||
|
}
|
||||||
|
// 处理拖拽过程
|
||||||
|
handleDragUpdate(taskId, newDate) {
|
||||||
|
// 校验新日期是否合法,可扩展
|
||||||
|
this.dataManager.updateTask(taskId, { start: newDate });
|
||||||
|
}
|
||||||
|
// 处理拖拽结束
|
||||||
|
handleDragEnd(taskId, newDate) {
|
||||||
|
// 最终更新数据
|
||||||
|
this.dataManager.updateTask(taskId, { start: newDate });
|
||||||
|
}
|
||||||
|
// 处理维度切换
|
||||||
|
handleSwitchDimension(dim) {
|
||||||
|
// 重新分组并通知视图层
|
||||||
|
this.dataManager.dimensions = [dim];
|
||||||
|
this.dataManager.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
57
components/Gantt/core/Layout.js
Normal file
57
components/Gantt/core/Layout.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Layout:布局计算器,负责任务条和依赖线的布局
|
||||||
|
export default class Layout {
|
||||||
|
constructor(tasks, config) {
|
||||||
|
this.tasks = tasks;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
// 计算任务条布局
|
||||||
|
computeTaskLayout() {
|
||||||
|
// 简单实现:每个任务一行,计算左侧(start)、宽度(end-start)
|
||||||
|
const layout = [];
|
||||||
|
this.tasks.forEach((task, idx) => {
|
||||||
|
const startPixel = this.dateToPixel(task.start);
|
||||||
|
const endPixel = this.dateToPixel(task.end);
|
||||||
|
layout.push({
|
||||||
|
id: task.id,
|
||||||
|
top: idx * 32,
|
||||||
|
left: startPixel,
|
||||||
|
width: endPixel - startPixel,
|
||||||
|
height: 28,
|
||||||
|
color: this.getTaskColor(task)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
// 计算依赖线布局(可扩展)
|
||||||
|
computeDependencyLines() {
|
||||||
|
// 返回依赖线的起止坐标
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// 工具:日期转像素
|
||||||
|
dateToPixel(date) {
|
||||||
|
const start = new Date(this.config.startDate);
|
||||||
|
const d = new Date(date);
|
||||||
|
const scaleMap = { day: 40, week: 80, month: 200, quarter: 600 };
|
||||||
|
const scale = scaleMap[this.config.timeScale] || 80;
|
||||||
|
let diff = 0;
|
||||||
|
if (this.config.timeScale === 'day') {
|
||||||
|
diff = (d - start) / (1000 * 3600 * 24);
|
||||||
|
} else if (this.config.timeScale === 'week') {
|
||||||
|
diff = (d - start) / (1000 * 3600 * 24 * 7);
|
||||||
|
} else if (this.config.timeScale === 'month') {
|
||||||
|
diff = (d.getFullYear() - start.getFullYear()) * 12 + (d.getMonth() - start.getMonth());
|
||||||
|
} else if (this.config.timeScale === 'quarter') {
|
||||||
|
diff = ((d.getFullYear() - start.getFullYear()) * 12 + (d.getMonth() - start.getMonth())) / 3;
|
||||||
|
}
|
||||||
|
return diff * scale;
|
||||||
|
}
|
||||||
|
// 工具:获取任务颜色
|
||||||
|
getTaskColor(task) {
|
||||||
|
const group = this.config.groupBy;
|
||||||
|
if (group && task.dimensions && task.dimensions[group]) {
|
||||||
|
const id = task.dimensions[group].id;
|
||||||
|
return this.config.colors && this.config.colors[group] && this.config.colors[group][id] ? this.config.colors[group][id] : '#409EFF';
|
||||||
|
}
|
||||||
|
return '#409EFF';
|
||||||
|
}
|
||||||
|
}
|
||||||
82
components/Gantt/core/TimeCalculator.js
Normal file
82
components/Gantt/core/TimeCalculator.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// TimeCalculator:时间轴计算器,负责时间与像素的转换、生成时间刻度等
|
||||||
|
export default class TimeCalculator {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.scaleMap = { day: 40, week: 80, month: 200, quarter: 600 }; // 每单位像素宽度
|
||||||
|
}
|
||||||
|
// 日期转像素坐标
|
||||||
|
dateToPixel(date) {
|
||||||
|
const start = new Date(this.config.startDate);
|
||||||
|
const d = new Date(date);
|
||||||
|
const scale = this.scaleMap[this.config.timeScale] || 80;
|
||||||
|
let diff = 0;
|
||||||
|
if (this.config.timeScale === 'day') {
|
||||||
|
diff = (d - start) / (1000 * 3600 * 24);
|
||||||
|
} else if (this.config.timeScale === 'week') {
|
||||||
|
diff = (d - start) / (1000 * 3600 * 24 * 7);
|
||||||
|
} else if (this.config.timeScale === 'month') {
|
||||||
|
diff = (d.getFullYear() - start.getFullYear()) * 12 + (d.getMonth() - start.getMonth());
|
||||||
|
} else if (this.config.timeScale === 'quarter') {
|
||||||
|
diff = ((d.getFullYear() - start.getFullYear()) * 12 + (d.getMonth() - start.getMonth())) / 3;
|
||||||
|
}
|
||||||
|
return diff * scale;
|
||||||
|
}
|
||||||
|
// 像素转日期(仅简单实现,实际可扩展)
|
||||||
|
pixelToDate(pixel) {
|
||||||
|
const start = new Date(this.config.startDate);
|
||||||
|
const scale = this.scaleMap[this.config.timeScale] || 80;
|
||||||
|
let d = new Date(start);
|
||||||
|
if (this.config.timeScale === 'day') {
|
||||||
|
d.setDate(start.getDate() + pixel / scale);
|
||||||
|
} else if (this.config.timeScale === 'week') {
|
||||||
|
d.setDate(start.getDate() + (pixel / scale) * 7);
|
||||||
|
} else if (this.config.timeScale === 'month') {
|
||||||
|
d.setMonth(start.getMonth() + pixel / scale);
|
||||||
|
} else if (this.config.timeScale === 'quarter') {
|
||||||
|
d.setMonth(start.getMonth() + (pixel / scale) * 3);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
// 生成时间轴刻度
|
||||||
|
getTimelineTicks() {
|
||||||
|
const ticks = [];
|
||||||
|
const start = new Date(this.config.startDate);
|
||||||
|
const end = new Date(this.config.endDate);
|
||||||
|
let cur = new Date(start);
|
||||||
|
while (cur <= end) {
|
||||||
|
ticks.push({
|
||||||
|
label: this.formatTick(cur),
|
||||||
|
date: new Date(cur)
|
||||||
|
});
|
||||||
|
if (this.config.timeScale === 'day') {
|
||||||
|
cur.setDate(cur.getDate() + 1);
|
||||||
|
} else if (this.config.timeScale === 'week') {
|
||||||
|
cur.setDate(cur.getDate() + 7);
|
||||||
|
} else if (this.config.timeScale === 'month') {
|
||||||
|
cur.setMonth(cur.getMonth() + 1);
|
||||||
|
} else if (this.config.timeScale === 'quarter') {
|
||||||
|
cur.setMonth(cur.getMonth() + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ticks;
|
||||||
|
}
|
||||||
|
// 格式化刻度
|
||||||
|
formatTick(date) {
|
||||||
|
if (this.config.timeScale === 'day') {
|
||||||
|
return date.toISOString().slice(5, 10);
|
||||||
|
} else if (this.config.timeScale === 'week') {
|
||||||
|
return 'W' + this.getWeekNumber(date);
|
||||||
|
} else if (this.config.timeScale === 'month') {
|
||||||
|
return date.getFullYear() + '-' + (date.getMonth() + 1);
|
||||||
|
} else if (this.config.timeScale === 'quarter') {
|
||||||
|
return date.getFullYear() + ' Q' + (Math.floor(date.getMonth() / 3) + 1);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// 获取周数
|
||||||
|
getWeekNumber(date) {
|
||||||
|
const firstDay = new Date(date.getFullYear(), 0, 1);
|
||||||
|
const dayOfYear = ((date - firstDay) / 86400000) + 1;
|
||||||
|
return Math.ceil(dayOfYear / 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
components/Gantt/core/types.js
Normal file
14
components/Gantt/core/types.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 任务数据模型
|
||||||
|
export const TaskModel = {
|
||||||
|
id: '', name: '', start: '', end: '', progress: 0,
|
||||||
|
dependencies: [], children: [],
|
||||||
|
dimensions: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 视图配置参数
|
||||||
|
export const ViewConfig = {
|
||||||
|
timeScale: 'week',
|
||||||
|
startDate: '', endDate: '',
|
||||||
|
groupBy: '', visibleDimensions: [],
|
||||||
|
colors: {}
|
||||||
|
};
|
||||||
0
components/Gantt/index.vue
Normal file
0
components/Gantt/index.vue
Normal file
84
components/Gantt/uniapp/DimensionPanel.vue
Normal file
84
components/Gantt/uniapp/DimensionPanel.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<view class="dimension-panel">
|
||||||
|
<!-- 维度切换 -->
|
||||||
|
<picker :range="dimensionOptions" range-key="label" @change="onSwitch">
|
||||||
|
<view class="dimension-switch">切换维度</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DimensionPanel',
|
||||||
|
props: {
|
||||||
|
dimensionOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
defaultColors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [
|
||||||
|
'#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399',
|
||||||
|
'#1abc9c', '#9b59b6', '#e67e22', '#e74c3c', '#34495e'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSwitch(e) {
|
||||||
|
const dim = this.dimensionOptions[e.detail.value]
|
||||||
|
this.$emit('switch', dim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dimension-panel {
|
||||||
|
width: 100vw;
|
||||||
|
background: #fafbfc;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
padding: 12px 0 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.discrete-bar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
height: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar-segment {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.dimension-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.legend-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.legend-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.dimension-switch {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #409EFF;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
146
components/Gantt/uniapp/Gantt.vue
Normal file
146
components/Gantt/uniapp/Gantt.vue
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<view class="gantt-container">
|
||||||
|
<!-- 顶部分组栏(维度选择) -->
|
||||||
|
<dimension-panel :dimensionOptions="dimensionOptions" @switch="onSwitchDimension" class="gantt-dimension-panel" />
|
||||||
|
<!-- 图例组件 -->
|
||||||
|
<legend :groups="taskGroups" :dimensionName="getCurrentDimensionName" />
|
||||||
|
<view class="gantt-body">
|
||||||
|
<!-- 右侧横向滚动区 -->
|
||||||
|
<scroll-view scroll-x class="gantt-scroll-x" @scroll="onScroll">
|
||||||
|
<view class="gantt-right-area">
|
||||||
|
<!-- 上方时间轴 -->
|
||||||
|
<timeline-header :config="timelineConfig" class="gantt-timeline-header" />
|
||||||
|
<!-- 下方任务条 -->
|
||||||
|
<gantt-canvas :tasks="visibleTasks" :layout="taskLayout" @drag="onTaskDrag" @task-click="onTaskClick" class="gantt-canvas-area" >
|
||||||
|
<template #task-bar="slotProps">
|
||||||
|
<slot name="task-bar" v-bind="slotProps" />
|
||||||
|
</template>
|
||||||
|
</gantt-canvas>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import DataManager from '../core/DataManager'
|
||||||
|
import TimeCalculator from '../core/TimeCalculator'
|
||||||
|
import Interaction from '../core/Interaction'
|
||||||
|
import Layout from '../core/Layout'
|
||||||
|
import TimelineHeader from './TimelineHeader.vue'
|
||||||
|
import DimensionPanel from './DimensionPanel.vue'
|
||||||
|
import GanttCanvas from './GanttCanvas.vue'
|
||||||
|
import LegendCom from './Legend.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Gantt',
|
||||||
|
components: { TimelineHeader, DimensionPanel, GanttCanvas, LegendCom },
|
||||||
|
props: {
|
||||||
|
tasks: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dataManager: null,
|
||||||
|
timeCalculator: null,
|
||||||
|
interaction: null,
|
||||||
|
layout: null,
|
||||||
|
taskGroups: [],
|
||||||
|
visibleTasks: [],
|
||||||
|
taskLayout: [],
|
||||||
|
timelineConfig: {},
|
||||||
|
dimensionOptions: this.config.visibleDimensions || [],
|
||||||
|
groupBy: this.config.groupBy // 新增响应式分组依据
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getCurrentDimensionName() {
|
||||||
|
const dim = this.dimensionOptions.find(d => d.prop === this.groupBy)
|
||||||
|
return dim ? dim.label || dim.name || dim.prop : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.dataManager = new DataManager(this.tasks, this.config.visibleDimensions)
|
||||||
|
this.timeCalculator = new TimeCalculator(this.config)
|
||||||
|
this.interaction = new Interaction(this.dataManager, this.timeCalculator)
|
||||||
|
this.layout = new Layout(this.tasks, this.config)
|
||||||
|
this.refresh()
|
||||||
|
this.dataManager.onChange(this.refresh)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refresh() {
|
||||||
|
this.taskGroups = this.dataManager.groupByDimension(this.groupBy)
|
||||||
|
this.visibleTasks = this.taskGroups.flatMap(g => g.tasks)
|
||||||
|
this.layout = new Layout(this.visibleTasks, this.config)
|
||||||
|
this.taskLayout = this.layout.computeTaskLayout()
|
||||||
|
const ticks = this.timeCalculator.getTimelineTicks().map(tick => {
|
||||||
|
return {
|
||||||
|
...tick,
|
||||||
|
left: this.timeCalculator.dateToPixel(tick.date)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.timelineConfig = { ticks }
|
||||||
|
},
|
||||||
|
onSwitchDimension(dim) {
|
||||||
|
this.groupBy = dim.prop
|
||||||
|
this.refresh()
|
||||||
|
},
|
||||||
|
onTaskDrag({ taskId, deltaX }) {
|
||||||
|
const task = this.tasks.find(t => t.id === taskId)
|
||||||
|
if (!task) return
|
||||||
|
const startPixel = this.timeCalculator.dateToPixel(task.start)
|
||||||
|
const newPixel = startPixel + deltaX
|
||||||
|
const newDate = this.timeCalculator.pixelToDate(newPixel)
|
||||||
|
this.interaction.handleDragUpdate(taskId, newDate.toISOString().slice(0, 10))
|
||||||
|
},
|
||||||
|
onTaskClick(task) {
|
||||||
|
this.$emit('task-click', task)
|
||||||
|
},
|
||||||
|
onScroll(e) {
|
||||||
|
// 不做 scrollLeft 回写,避免抖动
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.gantt-container {
|
||||||
|
width: 100%;
|
||||||
|
background: #f5f6fa;
|
||||||
|
}
|
||||||
|
.gantt-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
.gantt-dimension-panel {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
background: #fafbfc;
|
||||||
|
z-index: 2;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.gantt-scroll-x {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.gantt-right-area {
|
||||||
|
min-width: 800px;
|
||||||
|
}
|
||||||
|
.gantt-timeline-header {
|
||||||
|
height: 32px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.gantt-canvas-area {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
components/Gantt/uniapp/GanttCanvas.vue
Normal file
79
components/Gantt/uniapp/GanttCanvas.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<view class="gantt-canvas">
|
||||||
|
<view v-for="task in tasks" :key="task.id"
|
||||||
|
class="gantt-task-bar"
|
||||||
|
:style="{
|
||||||
|
top: layoutMap[task.id] && layoutMap[task.id].top + 'px',
|
||||||
|
left: layoutMap[task.id] && layoutMap[task.id].left + 'px',
|
||||||
|
width: layoutMap[task.id] && layoutMap[task.id].width + 'px',
|
||||||
|
height: layoutMap[task.id] && layoutMap[task.id].height + 'px',
|
||||||
|
background: layoutMap[task.id] && layoutMap[task.id].color
|
||||||
|
}"
|
||||||
|
@tap.stop="onTaskClick(task)"
|
||||||
|
>
|
||||||
|
<slot name="task-bar" :task="task" :layout="layoutMap[task.id]">
|
||||||
|
<!-- 默认渲染 -->
|
||||||
|
<text class="task-name">{{ task.name }}</text>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'GanttCanvas',
|
||||||
|
props: {
|
||||||
|
tasks: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
layoutMap: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
layout: {
|
||||||
|
immediate: true,
|
||||||
|
handler(val) {
|
||||||
|
const map = {}
|
||||||
|
val.forEach(item => { map[item.id] = item })
|
||||||
|
this.layoutMap = map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTaskClick(task) {
|
||||||
|
this.$emit('task-click', task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.gantt-canvas {
|
||||||
|
position: relative;
|
||||||
|
min-height: 400px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.gantt-task-bar {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.task-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
components/Gantt/uniapp/Legend.vue
Normal file
49
components/Gantt/uniapp/Legend.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<view class="legend-container">
|
||||||
|
<view class="legend-title">{{ dimensionName ? `当前维度:${dimensionName}` : '分组' }}</view>
|
||||||
|
<view class="legend-list">
|
||||||
|
<view v-for="group in groups" :key="group.value" class="legend-item">
|
||||||
|
<text>{{ group.label || group.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Legend',
|
||||||
|
props: {
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
dimensionName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.legend-container {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.legend-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.legend-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.legend-item {
|
||||||
|
background: #e3e6ef;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
components/Gantt/uniapp/TimelineHeader.vue
Normal file
36
components/Gantt/uniapp/TimelineHeader.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<view class="timeline-header">
|
||||||
|
<view v-for="tick in config.ticks" :key="tick.label" class="timeline-tick" :style="{ left: tick.left + 'px' }">
|
||||||
|
{{ tick.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TimelineHeader',
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.timeline-header {
|
||||||
|
position: relative;
|
||||||
|
height: 32px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.timeline-tick {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -385,14 +385,12 @@ export default {
|
|||||||
this.$refs.popupRef.open('bottom')
|
this.$refs.popupRef.open('bottom')
|
||||||
},
|
},
|
||||||
handleUpdate(row) {
|
handleUpdate(row) {
|
||||||
console.log('handleUpdate called', row);
|
|
||||||
getReportSchedule(row.scheduleId).then(res => {
|
getReportSchedule(row.scheduleId).then(res => {
|
||||||
this.form = res.data || {}
|
this.form = res.data || {}
|
||||||
this.$refs.popupRef.open('bottom')
|
this.$refs.popupRef.open('bottom')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleDelete(item) {
|
handleDelete(item) {
|
||||||
console.log('handleDelete called', item);
|
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: `确定要删除排产“${item.scheduleName}”吗?`,
|
content: `确定要删除排产“${item.scheduleName}”吗?`,
|
||||||
@@ -449,12 +447,6 @@ export default {
|
|||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
swipeActionClick(e, item) {
|
swipeActionClick(e, item) {
|
||||||
console.log('swipeActionClick e:', e);
|
|
||||||
console.log('swipeActionClick item:', item);
|
|
||||||
if (e && e.content) {
|
|
||||||
console.log('swipeActionClick e.content:', e.content);
|
|
||||||
console.log('swipeActionClick e.content.text:', e.content.text);
|
|
||||||
}
|
|
||||||
const text = e.content.text;
|
const text = e.content.text;
|
||||||
if (text === '编辑') {
|
if (text === '编辑') {
|
||||||
this.handleUpdate(item);
|
this.handleUpdate(item);
|
||||||
@@ -465,7 +457,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleStart(row) {
|
handleStart(row) {
|
||||||
console.log('handleStart called', row);
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const pad = n => n < 10 ? '0' + n : n;
|
const pad = n => n < 10 ? '0' + n : n;
|
||||||
const formatDate = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
const formatDate = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
@@ -526,8 +517,6 @@ export default {
|
|||||||
// 只保留唯一 projectId
|
// 只保留唯一 projectId
|
||||||
const uniqueProjectIds = Array.from(new Set(list.map(item => item.projectId)))
|
const uniqueProjectIds = Array.from(new Set(list.map(item => item.projectId)))
|
||||||
const categories = uniqueProjectIds.map(id => this.getProjectName(id))
|
const categories = uniqueProjectIds.map(id => this.getProjectName(id))
|
||||||
console.log(categories, '获取分类', list)
|
|
||||||
|
|
||||||
|
|
||||||
const data = list.map(item => ({
|
const data = list.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
Reference in New Issue
Block a user