From 5e1afb293ed2910f255218f6a77c485619aaa1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= Date: Wed, 16 Jul 2025 14:23:42 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=98=E7=89=B9=E5=9B=BE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E6=9C=89=E7=82=B9=E6=94=B9=E7=82=B8=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/Gantt/core/DataManager.js | 43 ++++++ components/Gantt/core/Interaction.js | 27 ++++ components/Gantt/core/Layout.js | 57 +++++++ components/Gantt/core/TimeCalculator.js | 82 ++++++++++ components/Gantt/core/types.js | 14 ++ components/Gantt/index.vue | 0 components/Gantt/uniapp/DimensionPanel.vue | 84 ++++++++++ components/Gantt/uniapp/Gantt.vue | 146 ++++++++++++++++++ components/Gantt/uniapp/GanttCanvas.vue | 79 ++++++++++ components/Gantt/uniapp/Legend.vue | 49 ++++++ components/Gantt/uniapp/TimelineHeader.vue | 36 +++++ .../reportSchedule/reportSchedule.vue | 11 -- 12 files changed, 617 insertions(+), 11 deletions(-) create mode 100644 components/Gantt/core/DataManager.js create mode 100644 components/Gantt/core/Interaction.js create mode 100644 components/Gantt/core/Layout.js create mode 100644 components/Gantt/core/TimeCalculator.js create mode 100644 components/Gantt/core/types.js create mode 100644 components/Gantt/index.vue create mode 100644 components/Gantt/uniapp/DimensionPanel.vue create mode 100644 components/Gantt/uniapp/Gantt.vue create mode 100644 components/Gantt/uniapp/GanttCanvas.vue create mode 100644 components/Gantt/uniapp/Legend.vue create mode 100644 components/Gantt/uniapp/TimelineHeader.vue diff --git a/components/Gantt/core/DataManager.js b/components/Gantt/core/DataManager.js new file mode 100644 index 0000000..f38ecde --- /dev/null +++ b/components/Gantt/core/DataManager.js @@ -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)); + } +} \ No newline at end of file diff --git a/components/Gantt/core/Interaction.js b/components/Gantt/core/Interaction.js new file mode 100644 index 0000000..9013939 --- /dev/null +++ b/components/Gantt/core/Interaction.js @@ -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(); + } +} \ No newline at end of file diff --git a/components/Gantt/core/Layout.js b/components/Gantt/core/Layout.js new file mode 100644 index 0000000..14e5cc9 --- /dev/null +++ b/components/Gantt/core/Layout.js @@ -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'; + } +} \ No newline at end of file diff --git a/components/Gantt/core/TimeCalculator.js b/components/Gantt/core/TimeCalculator.js new file mode 100644 index 0000000..0148f09 --- /dev/null +++ b/components/Gantt/core/TimeCalculator.js @@ -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); + } +} \ No newline at end of file diff --git a/components/Gantt/core/types.js b/components/Gantt/core/types.js new file mode 100644 index 0000000..d4c6107 --- /dev/null +++ b/components/Gantt/core/types.js @@ -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: {} +}; \ No newline at end of file diff --git a/components/Gantt/index.vue b/components/Gantt/index.vue new file mode 100644 index 0000000..e69de29 diff --git a/components/Gantt/uniapp/DimensionPanel.vue b/components/Gantt/uniapp/DimensionPanel.vue new file mode 100644 index 0000000..6dd2a9e --- /dev/null +++ b/components/Gantt/uniapp/DimensionPanel.vue @@ -0,0 +1,84 @@ + + + \ No newline at end of file diff --git a/components/Gantt/uniapp/Gantt.vue b/components/Gantt/uniapp/Gantt.vue new file mode 100644 index 0000000..3d6ffb5 --- /dev/null +++ b/components/Gantt/uniapp/Gantt.vue @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/components/Gantt/uniapp/GanttCanvas.vue b/components/Gantt/uniapp/GanttCanvas.vue new file mode 100644 index 0000000..aa8aee9 --- /dev/null +++ b/components/Gantt/uniapp/GanttCanvas.vue @@ -0,0 +1,79 @@ + + + \ No newline at end of file diff --git a/components/Gantt/uniapp/Legend.vue b/components/Gantt/uniapp/Legend.vue new file mode 100644 index 0000000..11f6c16 --- /dev/null +++ b/components/Gantt/uniapp/Legend.vue @@ -0,0 +1,49 @@ + + + + + \ No newline at end of file diff --git a/components/Gantt/uniapp/TimelineHeader.vue b/components/Gantt/uniapp/TimelineHeader.vue new file mode 100644 index 0000000..27d41a0 --- /dev/null +++ b/components/Gantt/uniapp/TimelineHeader.vue @@ -0,0 +1,36 @@ + + + \ No newline at end of file diff --git a/pages/workbench/reportSchedule/reportSchedule.vue b/pages/workbench/reportSchedule/reportSchedule.vue index bfc0c11..5bdb35d 100644 --- a/pages/workbench/reportSchedule/reportSchedule.vue +++ b/pages/workbench/reportSchedule/reportSchedule.vue @@ -385,14 +385,12 @@ export default { this.$refs.popupRef.open('bottom') }, handleUpdate(row) { - console.log('handleUpdate called', row); getReportSchedule(row.scheduleId).then(res => { this.form = res.data || {} this.$refs.popupRef.open('bottom') }) }, handleDelete(item) { - console.log('handleDelete called', item); uni.showModal({ title: '确认删除', content: `确定要删除排产“${item.scheduleName}”吗?`, @@ -449,12 +447,6 @@ export default { return options; }, 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; if (text === '编辑') { this.handleUpdate(item); @@ -465,7 +457,6 @@ export default { } }, handleStart(row) { - console.log('handleStart called', row); const now = new Date(); 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())}`; @@ -526,8 +517,6 @@ export default { // 只保留唯一 projectId const uniqueProjectIds = Array.from(new Set(list.map(item => item.projectId))) const categories = uniqueProjectIds.map(id => this.getProjectName(id)) - console.log(categories, '获取分类', list) - const data = list.map(item => ({ ...item,