活动介绍

<template> <div class="container"> <div class="search-wrapper"> <el-form class="search-from" :inline="true" label-width="70px" @submit.prevent="initData"> <el-form-item label="名称"> <el-input v-model="queryParams.name" placeholder="请输入名称" clearable/> </el-form-item> <el-form-item label="责任人"> <el-input v-model="queryParams.respPerson" placeholder="请输入责任人" clearable/> </el-form-item> <el-form-item> <el-button type="primary" @click="initData"> <i class="el-icon-search"></i> 搜索 </el-button> <el-button @click="resetQuery"> <i class="el-icon-refresh"></i> 重置 </el-button> <el-button type="primary" style="margin-left: 10px;" @click="toggleGantt" > {{ showGantt ? '收起甘特图' : '展开甘特图' }} </el-button> </el-form-item> </el-form> </div> <div ref="gantt" class="gantt-container"></div> <!-- 添加或修改治理计划对话框 --> <Dialog title="查看治理计划" :visible.sync="open" width="850px" height="600px" append-to-body> <el-form ref="form" :model="form" label-width="100px" disabled> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="code"> <el-input v-model="form.code" placeholder="请输入编号"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="text"> <el-input v-model="form.text" placeholder="请输入名称"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="上级计划" prop="parent"> <treeselect v-model="form.parent" :options="planOptions" :normalizer="normalizer" placeholder="选择上级计划" disabled /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="完成度(%)" prop="progress"> <el-input-number v-model="form.progress" placeholder="请输入完成百分比"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="责任人" prop="respPerson"> <span slot="label"> <span class="content-font">责任人</span> </span> <!-- 用户向导 --> <p style="margin-top:0px; float: left;font-size: 12px;"> {{ form.respPerson }} <el-button size="mini" type="primary" icon="el-icon-user" class="btn-wizard-trigger" style="margin-left: 10px" >选择用户 </el-button> </p> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="责任部门" prop="respDept"> <el-input v-model="form.respDept" placeholder="请输入责任部门"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划开始日期" prop="planStartDate"> <el-date-picker clearable v-model="form.planStartDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择计划开始日期"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际开始日期" prop="realStartDate"> <el-date-picker clearable v-model="form.realStartDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择实际开始日期"> </el-date-picker> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划结束时间" prop="planEndDate"> <el-date-picker clearable v-model="form.planEndDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择计划结束时间"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际结束日期" prop="realEndDate"> <el-date-picker clearable v-model="form.realEndDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择实际结束日期"> </el-date-picker> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划工期(天)" prop="planDuration"> <el-input v-model="form.planDuration" placeholder="请选择计划日期"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际工期(天)" prop="realDuration"> <el-input v-model="form.realDuration" placeholder="请选择实际日期"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" type="textarea" placeholder="请输入备注"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="反馈内容" prop="feedback"> <el-input v-model="form.feedback" type="textarea" placeholder="请输入内容"/> </el-form-item> </el-col> </el-row> </el-form> <div slot="buttons" class="dialog-footer"> <el-button @click="open = false">关 闭</el-button> </div> </Dialog> </div> </template> <script> import Dialog from '@/components/Dialog' import Treeselect from '@riophae/vue-treeselect' import '@riophae/vue-treeselect/dist/vue-treeselect.css' import {gantt} from "dhtmlx-gantt"; import "dhtmlx-gantt/codebase/dhtmlxgantt.css"; import {listPlan} from "@/api/dw/plan/planview"; export default { components: {Dialog, Treeselect}, name: "gantt", data() { return { tasks: { data: [], }, queryParams: { name: null, respPerson: null }, showGantt: true, // 状态控制甘特图显示 planOptions: [], open: false, // 控制详情弹窗显示 form: {} // 当前查看的任务 }; }, // 把携带的参数放到queryParams查询参数里 created() { // console.log(this.$route.params.id); }, methods: { // 查看任务详情 handleView(taskId) { // 根据任务ID查找任务详情 const task = this.tasks.data.find(item => item.id == taskId); if (task) { this.getTreeselect(); this.form = task; this.open = true; } }, toggleGantt() { this.showGantt = !this.showGantt; this.initData(); // 重新初始化甘特图 }, // 上级节点 getTreeselect() { listPlan().then(response => { const data = {uid: 0, name: '顶级节点', children: []}; data.children = this.handleTree(response.data, 'uid', 'parentUid') this.planOptions.push(data) }) }, normalizer(node) { if (node.children && !node.children.length) { delete node.children } return { id: node.uid, label: node.name, children: node.children } }, //开始时间-结束时间参数 DateDifference: function (strDateStart, strDateEnd) { var begintime_ms = Date.parse(new Date(strDateStart.replace(/-/g, "/"))); //begintime 为开始时间 var endtime_ms = Date.parse(new Date(strDateEnd.replace(/-/g, "/"))); // endtime 为结束时间 var date3 = endtime_ms - begintime_ms; //时间差的毫秒数 var days = Math.floor(date3 / (24 * 3600 * 1000)); return days; }, // 重置查询 resetQuery() { this.queryParams = { name: null, respPerson: null }; this.initData(); }, initData: function () { gantt.clearAll(); listPlan(this.queryParams).then((res) => { // 重新初始化甘特图配置 gantt.config.autosize = true; gantt.config.readonly = true; gantt.config.show_grid = true; this.tasks.data = res.data.map((item) => { let statusColor; //存在status字段 说明非一级菜单,判断阶段的具体类型 设置不同颜色 if (item.status == '1') { //冒烟 statusColor = "#84bd54" } else if (item.status == '2') { //单元 statusColor = "#fcca02" } else if (item.status == '3') { //回归 statusColor = "#dc1626" } else { statusColor = "#999999" } return { id: item.uid, parent: item.parent, text: item.name, start_date: item.planStartDate, duration: item.planDuration, open: true, //默认打开, toolTipsTxt: item.name, progress: item.schedule, status: item.status, code: item.code, respPerson: item.respPerson, respDept: item.respDept, planStartDate: item.planStartDate, planEndDate: item.planEndDate, realStartDate: item.realStartDate, realEndDate: item.realEndDate, planDuration: item.planDuration, realDuration: item.realDuration, remarks: item.remarks, feedback: item.feedback, color: statusColor, } }); //自适应甘特图的尺寸大小, 使得在不出现滚动条的情况下, 显示全部任务 gantt.config.autosize = true; //只读模式 gantt.config.readonly = true; //是否显示左侧树表格 gantt.config.show_grid = true; //表格列设置 gantt.config.columns = [ { name: "code", label: "编号", tree: true, width: "160", onrender: function (task, node) { node.setAttribute( "class", "gantt_cell gantt_last_cell gantt_cell_tree " + task.status ); }, }, { name: "status", label: "状态", align: "center", width: "80", template: function (task) { // 自定义状态列显示为状态灯 return `<div class="status-light" style="background-color: ${task.color}"></div>`; } }, {name: "text", label: "名称", align: "center", width: "180", hide: true}, {name: "progress", label: "完成度(%)", align: "center", width: "90", hide: true}, {name: "respPerson", label: "责任人", align: "center", width: "120", hide: true}, {name: "respDept", label: "责任部门", align: "center", width: "140", hide: true}, {name: "planStartDate", label: "计划开始日期", align: "center", width: "130", hide: true}, {name: "planEndDate", label: "计划结束时间", align: "center", width: "130", hide: true}, {name: "realStartDate", label: "实际开始日期", align: "center", width: "130", hide: true}, {name: "realEndDate", label: "实际结束日期", align: "center", width: "130", hide: true}, {name: "planDuration", label: "计划工期", align: "center", width: "90", hide: true}, {name: "realDuration", label: "实际工期", align: "center", width: "90", hide: true}, {name: "remarks", label: "备注", align: "center", width: "220", hide: true}, // { // name: "operate", // label: "操作", // align: "center", // width: "80", // template: function (task) { // return '<el-button size="mini" type="text" onclick=" window.vueInstance.handleView(\'' + task.id + '\')">查看</el-button>'; // } // } ]; var weekScaleTemplate = function (date) { var dateToStr = gantt.date.date_to_str("%m %d"); var endDate = gantt.date.add( gantt.date.add(date, 1, "week"), -1, "day" ); var weekNum = gantt.date.date_to_str("第 %W 周"); return weekNum(date); }; var daysStyle = function (date) { var dateToStr = gantt.date.date_to_str("%D"); if (dateToStr(date) == "六" || dateToStr(date) == "日") return "weekend"; return ""; }; gantt.config.subscales = [ { unit: "week", step: 1, template: weekScaleTemplate, }, { unit: "day", step: 1, format: "%d", }, ]; gantt.plugins({ tooltip: true, }); //设置鼠标放置显示事件 gantt.attachEvent("onGanttReady", function() { var tooltips = gantt.ext.tooltips; gantt.templates.tooltip_text = function(start, end, task) { return "编号:" + task.code + "<br/>" + "名称:" + task.text + "<br/>" + "计划开始:" + gantt.templates.tooltip_date_format(start) + "<br/>" + "工期:" + task.duration }; }); //设置任务条进度内容 gantt.templates.progress_text = function (start, end, task) { return ( "<div style='text-align:left;color:#fff;padding-left:20px'>" + task.progress + "% </div>" ); }; //任务条显示内容 gantt.templates.task_text = function (start, end, task) { // return task.text + '(' + task.duration + '天)'; return ( "<div style='text-align:center;color:#fff'>" + task.text + "(" + task.duration + "天)" + "</div>" ); }; //任务条上的文字大小 以及取消border自带样式 gantt.templates.task_class = function (start, end, item) { return item.$level == 0 ? "firstLevelTask" : "secondLevelTask"; }; gantt.config.layout = { css: "gantt_container", cols: [ { width: this.showGantt ? 800 : "100%", // 如果收起甘特图,左侧表格宽度占满 min_width: 300, //表格左侧最小宽度 rows: [ { view: "grid", scrollX: "gridScroll", scrollable: true, scrollY: "scrollVer", }, { view: "scrollbar", id: "gridScroll", group: "horizontal", }, ], }, ...(this.showGantt ? [{ resizer: true, width: 1, }, { rows: [ { view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer", }, { view: "scrollbar", id: "scrollHor", group: "horizontal", }, ] }] : []) ], }; //时间轴图表中,任务条形图的高度 // gantt.config.task_height = 28 //时间轴图表中,甘特图的高度 // gantt.config.row_height = 36 //时间轴图表中,如果不设置,只有行边框,区分上下的任务,设置之后带有列的边框,整个时间轴变成格子状。 gantt.config.show_task_cells = true; //当task的长度改变时,自动调整图表坐标轴区间用于适配task的长度 gantt.config.fit_tasks = true; gantt.config.min_column_width = 50; gantt.config.auto_types = true; gantt.config.xml_date = "%Y-%m-%d"; gantt.config.scale_unit = "month"; gantt.config.step = 1; gantt.config.date_scale = "%Y年%M"; gantt.config.start_on_monday = true; gantt.config.scale_height = 160; gantt.config.autoscroll = true; gantt.config.calendar_property = "start_date"; gantt.config.calendar_property = "end_date"; gantt.config.readonly = true; gantt.i18n.setLocale("cn"); // 初始化 gantt.init(this.$refs.gantt); // 数据解析 gantt.parse(this.tasks); // 添加双击行事件监听器 gantt.attachEvent("onTaskDblClick", function(id, e) { // 调用查看详情方法 window.vueInstance.handleView(id); return true; }); }); }, }, mounted() { // 将当前Vue实例赋值给window.vueInstance,供甘特图中调用 window.vueInstance = this; this.initData(); }, }; </script> <style lang="scss" scoped> .firstLevelTask { border: none; .gantt_task_content { font-size: 13px; } } .secondLevelTask { border: none; } .thirdLevelTask { border: 2px solid #da645d; color: #da645d; background: #da645d; } .milestone-default { border: none; background: rgba(0, 0, 0, 0.45); } .milestone-unfinished { border: none; background: #5692f0; } .milestone-finished { border: none; background: #84bd54; } .milestone-canceled { border: none; background: #da645d; } html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; } .container { height: 100%; width: 100%; position: relative; padding: 10px; .gantt_grid_head_cell { padding-left: 20px; text-align: left !important; font-size: 14px; color: #333; } .left-container { height: 100%; } .green, .yellow, .pink, .popular { .gantt_tree_icon.gantt_file { background: none; position: relative; &::before { content: ""; width: 10px; height: 10px; border-radius: 50%; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } } } .green { .gantt_tree_icon.gantt_file { &::before { background: #84bd54; } } } .yellow { .gantt_tree_icon.gantt_file { &::before { background: #fcca02; } } } .pink { .gantt_tree_icon.gantt_file { &::before { background: #da645d; } } } .popular { .gantt_tree_icon.gantt_file { &::before { background: #d1a6ff; } } } } .left-container { height: 100%; } .gantt_task_content { text-align: left; padding-left: 10px; } .gantt-container { height: calc(100% - 65px) !important; } // 状态灯样式 ::v-deep .gantt_grid_data .gantt_cell div.status-light { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin: 0 auto; } // 表格表头居中样式 ::v-deep .gantt_grid_head_cell { text-align: center !important; } .search-wrapper { text-align: left; padding: 10px 0; } .search-from { .el-form-item--mini.el-form-item, .el-form-item--small.el-form-item { margin-bottom: 3px; } } </style> 实现表格和甘特图的左右拖拽

时间: 2025-07-28 22:59:15 浏览: 205
<think>我们有一个Vue组件,它使用dhtmlx-gantt来显示甘特图。目前,组件中有一个搜索区域和一个甘特图区域。甘特图区域包含一个表格(左侧)和一个时间轴(右侧)。我们想要实现的功能是:允许用户通过拖拽分隔条来调整表格和甘特图时间轴区域的宽度。 在dhtmlx-gantt中,我们可以通过配置layout来定义结构,并且它内置了resizer(调整大小的分隔条)。在当前的代码中,我们已经有一个resizer,但是当收起甘特图时,我们移除了时间轴区域。现在我们需要确保在展开甘特图时,用户可以通过拖拽resizer来调整左右两侧的宽度。 然而,我们注意到在当前的layout配置中,我们根据`showGantt`变量动态地构建了列。当展开甘特图时,我们有两列:第一列是表格,第二列是时间轴,中间有一个resizer。当收起甘特图时,我们只有一列(表格)。 问题:目前resizer可能无法正常工作,或者我们想要确保resizer的拖拽功能正常,并且能够调整左右两部分的宽度。 解决方案: 1. 确保在layout配置中,resizer被正确添加。在当前的代码中,当`showGantt`为true时,我们在两列之间添加了一个resizer(宽度为1px,并且有resizer: true)。这应该会启用拖拽调整宽度。 2. 但是,我们可能还需要保存调整后的宽度,以便在切换甘特图显示状态后,再次展开时能够保持用户调整的宽度。 然而,dhtmlx-gantt的layout resizer在调整大小时会改变相邻两列的宽度。但是,我们目前没有保存调整后的宽度。所以,当收起再展开后,宽度会重置。 因此,我们需要: - 在调整宽度时,将左侧表格的宽度保存到组件的data中(例如,`gridWidth`)。 - 在初始化layout时,如果存在保存的宽度,则使用该宽度。 修改步骤: 1. 在data中定义一个变量来保存左侧表格的宽度,例如`gridWidth: 800`(默认值800)。 2. 在layout配置中,将左侧宽度设置为保存的宽度(如果有的话)或者默认值。 3. 监听gantt的resizer拖拽事件,在拖拽结束时更新`gridWidth`。 4. 当切换甘特图显示状态(收起/展开)时,如果收起,则表格占满;如果展开,则使用保存的宽度。 但是,dhtmlx-gantt的layout resizer在拖拽时并不会直接改变我们设置的宽度值,我们需要通过事件来捕获宽度的变化。 我们可以使用gantt的`onResize`事件来监听resizer的拖拽。但是注意,这个事件会在拖拽过程中不断触发,我们可能只需要在拖拽结束时更新一次。 然而,dhtmlx-gantt并没有直接提供resize结束的事件。我们可以使用`onResize`事件(在拖拽过程中触发)来更新宽度,但这样会频繁更新。或者,我们可以使用`onAfterResize`事件(如果存在的话)。但是,查看dhtmlx-gantt文档,我们发现有一个`onResizeEnd`事件。 因此,我们可以: gantt.attachEvent("onResizeEnd", function(columnId, side, width){ // 这里columnId是调整的列(resizer)的id,但是我们调整的是左侧的宽度,所以我们可以获取左侧的宽度 // 注意:在layout中,左侧列是我们定义的第一列,我们可以通过gantt的API获取其宽度 // 或者,在onResizeEnd事件中,width参数是调整后resizer左侧列的宽度(即第一列的宽度)吗? // 实际上,onResizeEnd事件的参数是:id, side, width,其中id是resizer的id(在layout中我们并没有给resizer设置id),side是调整的方向('left'或'right'),width是调整后的宽度(像素值)。 // 但是,我们并没有给resizer设置id,所以可能无法直接获取。我们可以通过gantt的DOM操作来获取左侧的宽度。 // 另一种思路:在resize过程中,我们可以通过gantt的配置对象获取第一列的宽度,因为gantt会实时更新DOM的宽度。 // 实际上,我们可以通过gantt.$root.querySelector('.gantt_grid')来获取左侧表格的DOM元素,然后获取其宽度。 // 但是,由于我们是在事件回调中,我们可以这样: let gridWidth = gantt.$root.querySelector('.gantt_grid').offsetWidth; // 然后保存这个宽度到Vue实例的gridWidth变量中。 // 注意:由于我们是在gantt的回调中,this指向gantt,所以我们需要使用Vue实例的引用(比如之前我们设置的window.vueInstance,或者使用闭包保存this)。 }); 但是,我们也可以使用gantt.ext.responsiveLayout.getColumnWidth(columnIndex)来获取列的宽度。但是,这个扩展responsiveLayout需要先启用。 考虑到简单性,我们直接通过DOM获取左侧表格的宽度。 修改代码: 步骤1:在data中定义gridWidth,并设置默认值800。 步骤2:在layout配置中,左侧列的宽度使用gridWidth(如果showGantt为true,则使用gridWidth;如果为false,则使用100%)。 步骤3:在gantt初始化后,注册onResizeEnd事件,更新gridWidth。 步骤4:在切换甘特图显示状态(toggleGantt)时,如果收起,则gridWidth应该保留当前值,以便下次展开时使用;如果展开,则使用之前保存的gridWidth(在layout配置中已经使用了gridWidth)。 注意:在收起甘特图时,我们设置左侧宽度为100%,这不会影响gridWidth的值,因为gridWidth保存的是展开时左侧的宽度。 具体代码修改: 1. 在data中增加gridWidth: 800(初始值800)。 2. 在layout配置中,将左侧宽度改为: width: this.showGantt ? this.gridWidth : "100%", 3. 在mounted或initData中,在gantt初始化后,添加事件监听: gantt.attachEvent("onResizeEnd", function(id, side, width) { // 获取左侧表格的实际宽度 const gridContainer = this.$refs.gantt.querySelector('.gantt_grid'); if (gridContainer) { const gridWidth = gridContainer.offsetWidth; // 更新Vue实例的gridWidth window.vueInstance.gridWidth = gridWidth; } }); 注意:由于事件回调中this指向gantt,所以我们使用window.vueInstance(我们在mounted中已经将this赋值给window.vueInstance)。 4. 但是,在收起甘特图时,我们不需要调整,因为此时没有resizer。 另外,我们还需要注意:当用户调整窗口大小时,我们可能需要重新调整布局?dhtmlx-gantt的responsiveLayout扩展可以处理,但我们这里不讨论。 但是,我们可能会遇到一个问题:在收起甘特图后再展开,我们希望使用之前保存的gridWidth,但是gantt在重新初始化时,layout配置中使用了gridWidth,所以应该没问题。 但是,我们还需要考虑:如果用户从未调整过宽度,那么使用默认值800。调整后,gridWidth会更新。 还有一个问题:在拖拽resizer时,我们可能希望实时更新gridWidth?但是,onResizeEnd事件是在拖拽结束时触发的,所以这样保存是合理的。 但是,我们也可以考虑在拖拽过程中实时更新gridWidth?没有必要,因为拖拽过程中宽度变化是连续的,我们只需要在结束时保存即可。 修改后的代码: 在initData方法中,在gantt.init之后,添加事件监听: gantt.attachEvent("onResizeEnd", function(id, side, width) { // 获取左侧表格的宽度 const gridContainer = document.querySelector('.gantt_grid'); // 注意:因为gantt容器内只有一个gantt,所以可以直接用类名 // 但是为了准确,我们可以使用this.$refs.gantt来限定范围 const container = window.vueInstance.$refs.gantt; if (container) { const gridContainer = container.querySelector('.gantt_grid'); if (gridContainer) { const gridWidth = gridContainer.offsetWidth; window.vueInstance.gridWidth = gridWidth; } } }); 注意:由于事件监听只需要注册一次,所以我们在initData中每次重新初始化甘特图时都会重新注册。但是,每次初始化会清除之前的事件监听吗?dhtmlx-gantt的attachEvent会叠加事件监听器,所以我们需要在每次初始化前先销毁之前的gantt实例?或者,在注册之前先detachEvent。 我们可以使用: gantt.detachEvent("onResizeEnd"); // 先移除之前的事件监听器,再添加新的 但是,在第一次时,可能没有监听器,所以可以这样: var eventId = gantt.attachEvent("onResizeEnd", ...); // 但是这样我们无法在下次初始化前移除,因为eventId是上次的。 另一种做法:在mounted中只注册一次,因为initData会多次调用,但事件只需要注册一次。我们可以将事件注册放在mounted中,但确保在gantt.init之后。 但是,我们是在initData中调用gantt.init,所以我们在gantt.init之后注册事件。但是,如果多次调用initData,就会多次注册事件。所以,我们需要确保只注册一次。 我们可以设置一个标志位,例如: if (!this.resizeEventRegistered) { gantt.attachEvent(...); this.resizeEventRegistered = true; } 或者在mounted中,在initData调用之后注册事件。但是,第一次初始化是在mounted中调用initData,然后我们在mounted中注册事件?不行,因为initData是异步的(有API请求)。 因此,我们在initData的then回调中,在gantt.init之后注册事件,并且确保只注册一次。 修改:在data中增加resizeEventRegistered: false 在initData的then回调中,在gantt.init之后: if (!this.resizeEventRegistered) { gantt.attachEvent("onResizeEnd", this.handleResizeEnd); this.resizeEventRegistered = true; } 然后,在methods中定义handleResizeEnd方法: handleResizeEnd(id, side, width) { const container = this.$refs.gantt; if (container) { const gridContainer = container.querySelector('.gantt_grid'); if (gridContainer) { this.gridWidth = gridContainer.offsetWidth; } } } 注意:这里我们使用Vue实例的handleResizeEnd方法,这样在方法内可以直接通过this访问Vue实例。 但是,attachEvent会将事件上下文设置为gantt,所以我们在handleResizeEnd中不能使用this。因此,我们可以使用箭头函数,或者在attachEvent时绑定this。 我们可以这样: gantt.attachEvent("onResizeEnd", (id, side, width) => { const container = this.$refs.gantt; if (container) { const gridContainer = container.querySelector('.gantt_grid'); if (gridContainer) { this.gridWidth = gridContainer.offsetWidth; } } }); 这样,我们就不需要单独定义handleResizeEnd方法,而且可以确保只注册一次。 修改后的initData方法中: if (!this.resizeEventRegistered) { gantt.attachEvent("onResizeEnd", (id, side, width) => { const container = this.$refs.gantt; if (container) { const gridContainer = container.querySelector('.gantt_grid'); if (gridContainer) { // 保存到gridWidth this.gridWidth = gridContainer.offsetWidth; } } }); this.resizeEventRegistered = true; } 另外,在切换甘特图显示状态时(toggleGantt方法),我们调用initData重新初始化甘特图,此时左侧宽度会根据showGantt和gridWidth进行设置。 但是,我们还需要注意:当gridWidth改变时,gantt不会自动重新布局,因为我们在layout配置中使用了this.gridWidth,但initData只会在初始化时设置一次。所以,在gridWidth改变后,我们需要重新初始化甘特图?但是,gridWidth的改变是在拖拽结束后,而拖拽结束后gantt已经调整了布局,所以我们只需要保存这个值,以便下次展开时使用。当前展开状态下,gantt已经调整好了,所以不需要重新初始化。 但是,在拖拽过程中,gantt已经通过自己的布局机制调整了宽度,所以我们保存的gridWidth就是当前的宽度。在下次展开时,我们使用这个宽度。 因此,我们不需要在gridWidth改变时做任何额外操作。 最后,我们修改layout配置中的左侧宽度: width: this.showGantt ? this.gridWidth : "100%", 这样,在展开时,左侧宽度为gridWidth(用户调整后的值),收起时,左侧宽度为100%。 但是,我们还需要注意:当gridWidth改变时,Vue会重新渲染,但gantt是通过initData初始化的,所以gridWidth的改变不会自动触发重新初始化。因此,我们不需要担心,因为gridWidth的改变不会影响当前显示的甘特图(因为甘特图已经初始化了,而且我们是在拖拽结束后才改变gridWidth的,此时甘特图已经调整了宽度,我们只是保存这个值以便下次使用)。 但是,在拖拽过程中,我们改变了gridWidth,然后Vue重新渲染,但甘特图是独立于Vue的DOM操作,所以不会冲突。 所以,按照上述步骤修改代码即可。 注意:在layout配置中,我们使用了gridWidth,所以当gridWidth变化时,我们不会重新构建gantt,直到下一次调用initData(比如切换收起/展开,或者搜索、重置等操作)。这是符合预期的。 代码修改: 1. 在data中增加: gridWidth: 800, // 默认左侧宽度 resizeEventRegistered: false, // 是否已注册resize事件 2. 在layout配置中,将左侧宽度改为: width: this.showGantt ? this.gridWidth : "100%", 3. 在initData方法中,在gantt.init(this.$refs.gantt)之后,添加: if (!this.resizeEventRegistered) { gantt.attachEvent("onResizeEnd", (id, side, width) => { const container = this.$refs.gantt; if (container) { const gridContainer = container.querySelector('.gantt_grid'); if (gridContainer) { this.gridWidth = gridContainer.offsetWidth; } } }); this.resizeEventRegistered = true; } 4. 注意:如果页面中有多个gantt实例,那么需要更精确的获取左侧表格的容器。我们使用this.$refs.gantt来限定范围。 但是,在dhtmlx-gantt中,左侧表格的容器具有类名"gantt_grid",所以我们在这个容器内查找。 另外,在收起甘特图时,左侧表格的宽度是100%,此时用户不能拖拽(因为没有resizer),所以不需要处理。 这样,我们就实现了保存用户调整的左侧表格宽度。 但是,我们还需要测试:当拖拽resizer时,左侧宽度改变,然后我们保存。然后收起甘特图,再展开,左侧宽度应该是之前调整的宽度。 注意:在收起甘特图时,我们调用initData重新初始化甘特图,此时布局变成只有一列(宽度100%)。当再次展开时,我们使用之前保存的gridWidth,所以左侧宽度会恢复。 但是,在收起时,左侧表格宽度为100%,而gridWidth保存的是展开时调整的宽度。所以,在收起时,我们不改变gridWidth的值,这样在展开时才能恢复。 所以,我们的逻辑是符合要求的。 最后,我们还需要注意:在初始化时,如果gridWidth没有设置,则使用默认值800。在拖拽调整后,gridWidth更新。在后续的任何操作中(比如搜索、重置),调用initData重新初始化甘特图,左侧表格宽度都会使用最新的gridWidth。 因此,我们修改代码。 但是,我们还需要注意:在调整resizer时,可能会调整得非常小,或者非常大。我们需要设置一个最小宽度,避免用户调整得过小。 在layout配置中,我们可以为左侧列设置min_width,例如: min_width: 300, // 最小宽度 这样,在调整时,gantt会限制最小宽度。 我们已经在layout配置中设置了min_width: 300,所以不需要额外处理。 修改后的代码示例(只显示修改部分):</think># 实现表格和甘特图的左右拖拽功能 在dhtmlx-gantt中实现表格和甘特图区域的左右拖拽功能,需要正确配置布局并处理拖拽事件。以下是完整的解决方案: ## 解决方案分析 1. **布局配置**:使用dhtmlx-gantt的layout配置定义左右两个区域(表格和甘特图)以及它们之间的可拖拽分隔条 2. **宽度保存**:保存用户调整后的宽度值,以便在组件重新渲染时保持 3. **事件处理**:监听拖拽结束事件,更新保存的宽度值 4. **响应式处理**:确保在不同屏幕尺寸下布局合理 ## 修改后的Vue组件代码 ```html <template> <div class="container"> <!-- 搜索区域保持不变 --> <div ref="gantt" class="gantt-container"></div> <!-- 对话框代码保持不变 --> </div> </template> <script> // 导入部分保持不变 export default { // 组件定义保持不变 data() { return { // 新增数据属性 gridWidth: 800, // 左侧表格初始宽度 resizeEventRegistered: false, // 拖拽事件注册状态 // 其他数据属性保持不变... }; }, methods: { // 其他方法保持不变... initData: function () { gantt.clearAll(); listPlan(this.queryParams).then((res) => { // 甘特图配置保持不变... // 布局配置 - 关键修改部分 gantt.config.layout = { css: "gantt_container", cols: [ { width: this.showGantt ? this.gridWidth : "100%", // 使用保存的宽度 min_width: 300, // 表格左侧最小宽度 rows: [ { view: "grid", scrollX: "gridScroll", scrollable: true, scrollY: "scrollVer" }, { view: "scrollbar", id: "gridScroll", group: "horizontal" } ] }, ...(this.showGantt ? [ { resizer: true, // 启用拖拽调整 width: 1, // 分隔条宽度 }, { rows: [ { view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" }, { view: "scrollbar", id: "scrollHor", group: "horizontal" } ] } ] : []) ] }; // 其他甘特图配置保持不变... // 初始化甘特图 gantt.init(this.$refs.gantt); gantt.parse(this.tasks); // 注册拖拽结束事件 - 关键修改部分 if (!this.resizeEventRegistered) { gantt.attachEvent("onResizeEnd", (id, side, width) => { const container = this.$refs.gantt; if (container) { const gridContainer = container.querySelector('.gantt_grid'); if (gridContainer) { // 保存新的宽度值 this.gridWidth = gridContainer.offsetWidth; } } }); this.resizeEventRegistered = true; } // 其他事件监听保持不变... }); }, }, mounted() { window.vueInstance = this; this.initData(); }, }; </script> <style lang="scss" scoped> /* 样式保持不变,但添加分隔条样式 */ .gantt_resizer { background-color: #e0e0e0; cursor: col-resize; width: 5px; z-index: 1; &:hover { background-color: #4a9ff9; } } /* 其他样式保持不变 */ </style> ``` ## 代码解释 ### 关键修改点 1. **布局配置**: - 左侧表格宽度使用`this.gridWidth`变量(用户调整后会更新) - 中间添加了`resizer: true`的列作为可拖拽分隔条 - 右侧是甘特图区域 2. **拖拽事件处理**: ```javascript gantt.attachEvent("onResizeEnd", (id, side, width) => { // 获取左侧表格的实际宽度 const gridContainer = this.$refs.gantt.querySelector('.gantt_grid'); if (gridContainer) { // 更新保存的宽度值 this.gridWidth = gridContainer.offsetWidth; } }); ``` - 监听`onResizeEnd`事件(拖拽结束时触发) - 获取左侧表格的实际宽度并保存到`gridWidth` 3. **宽度保存**: - 使用`gridWidth`变量保存用户调整后的宽度 - 在重新渲染甘特图时使用这个值,确保宽度保持一致 4. **分隔条样式**: ```css .gantt_resizer { background-color: #e0e0e0; cursor: col-resize; width: 5px; z-index: 1; &:hover { background-color: #4a9ff9; } } ``` - 添加了分隔条的基本样式和悬停效果 - 设置`cursor: col-resize`显示调整大小的光标 ### 实现效果 1. 用户可以通过拖拽中间的竖条调整左右区域宽度 2. 调整后的宽度会被保存,在重新渲染甘特图时保持不变 3. 分隔条在悬停时有明显的视觉反馈 4. 收起/展开甘特图时,布局能正确适应 ## 相关问题
阅读全文

相关推荐

解读学习代码: <script setup> import { carManGetService, carManStartService, carManStopService, carManDelService, vidallDataGetService } from '@/api/cloud.js' import { Plus, Delete, Edit, Search, Refresh } from '@element-plus/icons-vue' import { ref } from 'vue' import AddCar from './components/AddCar.vue' import SureDevice from './components/SureDevice.vue' import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import CarvidSelect from '@/views/manage/components/CarvidSelect.vue' import DevcidSelect from '@/views/manage/components/DevcidSelect.vue' import { Picture as IconPicture } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox, ElEmpty } from 'element-plus' const tableRef = ref([]) const carManDataList = ref([]) const total = ref(0) const updateDialog = ref() const deviceDialog = ref() // const editDialog = ref() const tableSelection = ref([]) const loading = ref(true) //定义请求参数对象 const params = ref({ pageNum: 1, pageSize: 10, cid: '', vid: '', key: '', status: '', enableStatus: '' }) //导入列表数据 const getCarManDataList = async () => { const res = await carManGetService(params.value) // 增加健壮性判断 if (res && res.data && res.data.data) { carManDataList.value = res.data.data.records total.value = res.data.data.total } else { carManDataList.value = [] total.value = 0 } loading.value = false } getCarManDataList() // 获取所属车辆 const vidDataList = ref([]) const getRoleDataList = async () => { const res = await vidallDataGetService() vidDataList.value = res.data.data } getRoleDataList() //搜索 const onSearch = () => { params.value.pageNum = 1 getCarManDataList() } //重置 const onReset = () => { params.value.pageNum = 1 params.value.cid = '' params.value.vid = '' params.value.key = '' params.value.enableStatus = '' params.value.status = '' getCarManDataList() } //多选框 let ids = [] const handleSelectionChange = (selects) => { ids = [] tableSelection.value = selects selects.forEach((rows) => { ids.push(rows.id) }) console.log(ids) console.log(tableSelection.value) } // 表格样式 // const rowStyle = (val) => { // // console.log('1111', val) // if (val.rowIndex % 2 === 0) { // // 奇数行 // return { // 'text-align': 'center', // color: '#fff', // background: '#2e3a54' // // color: 'red' // } // } // // 偶数行 // else // return { // 'text-align': 'center', // color: '#fff', // backgroundColor: '#1f2940' // } // } // 搜索式输入框 const handleSearch = () => {} //新增 const onAddMessage = () => { updateDialog.value.open({}) } //编辑 const onEditTable = (row) => { console.log(row.status) updateDialog.value.open(row) } // 添加或者编辑 成功的回调 const onSuccess = (type) => { if (type === 'add') { // // 如果是添加,最好渲染最后一页 // const lastPage = Math.ceil((total.value + 1) / params.value.pageSize) // // 更新成最大页码数,再渲染 // params.value.pageNum = lastPage params.value.pageNum = 1 } getCarManDataList() } //启用禁用 const onStart = async (row) => { // console.log(row.status) // console.log(row.id) if (row.enableStatus === 0) { // params.value.id = row.id const res = await carManStartService({ id: row.id }) if (res.data.code === 200) { // console.log('错误', res) ElMessage.success('车辆已启用') } else if (res.data.code !== 200) { ElMessage.error(${res.data.msg}) } } else if (row.enableStatus === 1) { // params.value.id = row.id const res = await carManStopService({ id: row.id }) if (res.data.code === 200) { // console.log('错误', res) ElMessage.success('车辆已禁用') } else if (res.data.code !== 200) { ElMessage.error(${res.data.msg}) } } getCarManDataList() } // 绑定台架 const inSureDevice = async (row) => { deviceDialog.value.open(row) } // 删除逻辑 const onDeleteMessage = async () => { // 提示用户是否要删除 await ElMessageBox.confirm('此操作将永久删除该数据, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) const res = await carManDelService(ids) if (res.data.code == 200) { ElMessage.success('删除成功') getCarManDataList() tableRef.value.clearSelection() } else if (res.data.code !== 200) { ElMessage.error(${res.data.msg}) } } //处理分页逻辑 const handleSizeChange = (size) => { //每页条数改变后,从第一页开始渲染 params.value.pageNum = 1 params.value.pageSize = size getCarManDataList() } const handleCurrentChange = (page) => { params.value.pageNum = page getCarManDataList() } zhCn.el.pagination = { goto: '跳至', pageClassifier: '页', total: '共 {total} 条', pagesize: '条/页' } </script> <template> <el-breadcrumb separator="/"> <el-breadcrumb-item ></el-breadcrumb-item> <el-breadcrumb-item>车辆管理</el-breadcrumb-item> <el-breadcrumb-item :to="{ path: '/manage/car' }" >车辆信息</el-breadcrumb-item > </el-breadcrumb> <AddCar ref="updateDialog" @success="onSuccess"></AddCar> <SureDevice ref="deviceDialog" @success="onSuccess"></SureDevice> <el-form inline class="search_form"> <el-form-item label="关键字:"> <el-input v-model="params.key" placeholder="请输入车辆名称、车牌号" clearable @input="handleSearch" ></el-input> </el-form-item> <el-form-item label="车辆编号:"> <CarvidSelect v-model="params.vid" placeholder="请选择车辆编号" clearable @input="handleSearch" ></CarvidSelect> </el-form-item> <el-form-item label="驾驶舱编号:"> <DevcidSelect v-model="params.cid" placeholder="请选择驾驶舱编号" clearable @input="handleSearch" ></DevcidSelect> </el-form-item> <el-form-item label="在线状态:"> <el-select v-model="params.status" placeholder="请选择在线状态" clearable > <el-option label="离线" :value="0"></el-option> <el-option label="在线" :value="1"></el-option> </el-select> </el-form-item> <el-form-item label="启用状态:"> <el-select v-model="params.enableStatus" placeholder="请选择启用状态" clearable > <el-option label="禁用" :value="0"></el-option> <el-option label="启用" :value="1"></el-option> </el-select> </el-form-item> <el-form-item> <el-button @click="onSearch" style="background-color: #165dff; color: #fff" ><el-icon style="padding-right: 5px"><Search /></el-icon> 搜索</el-button > <el-button @click="onReset" style="background-color: #fff; color: #000"> <el-icon style="padding-right: 5px"><Refresh /></el-icon> 重置</el-button > </el-form-item> </el-form> <el-form class="function_form" inline style="justify-content: end; display: flex" > <el-form-item style="margin-right: 0"> <el-button @click="onAddMessage" style="background-color: #165dff; color: #fff" > <el-icon style="padding-right: 5px"></el-icon> 新增</el-button > <el-button @click="onDeleteMessage" :disabled="tableSelection.length === 0" style="background-color: #f53f3f; color: #fff" > <el-icon style="padding-right: 5px"><Delete /></el-icon> 删除</el-button > </el-form-item> </el-form> <el-table id="table" ref="tableRef" :row-key="(row) => row.id" @selection-change="handleSelectionChange" :data="carManDataList" style="width: 100%; margin-top: 10px" :header-cell-style="{ 'text-align': 'center', background: '#f2f3f5', color: '#000' }" :cell-style="{ 'text-align': 'center', color: '#000' }" > <el-table-column fixed type="selection" :reserve-selection="true" width="40" /> <el-table-column prop="vid" label="车辆编号" min-width="160" :show-overflow-tooltip="true" ><template #default="{ row }"> {{ vidDataList.find((item) => row.vid == item.id)?.name || '-' }} - </template> </el-table-column> <el-table-column prop="cid" label="驾驶舱编号" min-width="100" :show-overflow-tooltip="true" ><template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="name" label="车辆名称" min-width="120" :show-overflow-tooltip="true" ><template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="plateNumber" label="车辆牌照" min-width="120" :show-overflow-tooltip="true" ><template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="picture" label="车辆图片" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> <el-image v-if="row.picture" :src="row.picture" alt="" style="width: 50px; height: 50px" > <template #error> <el-icon><icon-picture /></el-icon> </template> </el-image> - </template> </el-table-column> <el-table-column prop="status" label="在线状态" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> <el-tag v-if="row.status === 0" type="info">离线</el-tag> <el-tag v-if="row.status === 1" type="success">在线</el-tag> - </template> </el-table-column> <el-table-column prop="enableStatus" label="启用状态" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> <el-tag v-if="row.enableStatus === 0" type="danger">禁用</el-tag> <el-tag v-if="row.enableStatus === 1" type="success">启用</el-tag> - </template> </el-table-column> <el-table-column prop="hasGateway" label="是否具备网关" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="model" label="型号" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="manufactureDate" label="出厂日期" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="organizeId" label="组织号" min-width="110" :show-overflow-tooltip="true" > <template #default="{ row }"> - </template> </el-table-column> <el-table-column prop="created" label="创建时间" min-width="170" /> <el-table-column prop="last" label="更新时间" min-width="170" /> <el-table-column label="操作" width="260" fixed="right"> <template #default="{ row }"> <el-button class="operate" style="color: #165dff" link @click="onEditTable(row)" ><el-icon style="padding-right: 3px"><Edit /></el-icon >编辑</el-button > <el-text class="mx-1" style="color: #165dff; padding-left: 3px" >|</el-text > <el-button link @click="onStart(row)"> <el-button link v-if="row.enableStatus === 0" style="color: #00ff48" >启用</el-button > <el-button class="operate" link v-if="row.enableStatus === 1" style="color: #ff4646" >禁用</el-button > </el-button> <el-text class="mx-1" style="color: #165dff; padding-left: 3px" >|</el-text > <el-button link @click="inSureDevice(row)"> <el-button link style="color: #165dff" >绑定驾驶舱</el-button > </el-button> </template> </el-table-column> <template #empty> <el-empty description="没有数据"></el-empty> </template> </el-table> <el-pagination :teleported="false" v-model:current-page="params.pageNum" v-model:page-size="params.pageSize" :page-sizes="[5, 10]" layout="total,prev, pager, next,sizes" :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" style="margin-top: 40px; margin-bottom: 40px; justify-content: end" /> </template> <style lang="scss" scoped> .operate { margin-left: 0px !important; } // :deep .el-input__wrapper { // background-color: #f2f3f5; // } // 面包屑 .bread { height: 50px; display: flex; align-items: center; padding-left: 20px; :deep .el-breadcrumb__item:last-child .el-breadcrumb__inner { cursor: pointer !important; } } :deep .el-breadcrumb { display: flex; align-items: center; .el-breadcrumb__inner { color: #4e5969; } .el-breadcrumb__item { &:nth-child(1), &:last-child { .el-breadcrumb__inner { color: #1d2129; } } } } // 表格 :deep .el-scrollbar__wrap { max-height: 48vh; } // 分页 :deep .el-pagination { .el-pager li { // background-color: #fff; color: #000; // border: 1px solid #e5e5ea; border-radius: 2px; min-width: 32px; height: 32px; margin: 0 5px; } .el-pager li:hover { background-color: #e8f3ff; color: #165dff; } .el-pager li.is-active { background-color: #e8f3ff !important; color: #165dff !important; font-size: medium; } .btn-next, .btn-prev { // background-color: #fff; // border: 1px solid #e5e5ea; // border-radius: 5px; width: 32px; height: 32px; } } :deep .image-slot { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background: var(--el-fill-color-light); color: var(--el-text-color-secondary); font-size: 20px; } </style>

<template> <el-form class="search-from" :inline="true" label-width="70px" @submit.prevent="initGantt"> <el-form-item label="名称"> <el-input v-model="queryParams.name" placeholder="请输入名称" clearable/> </el-form-item> <el-form-item label="责任人"> <el-input v-model="queryParams.respPerson" placeholder="请输入责任人" clearable/> </el-form-item> <el-form-item> <el-button type="primary" @click="initGantt"> 搜索 </el-button> <el-button @click="resetQuery"> 重置 </el-button> <el-button type="primary" style="margin-left: 10px;" @click="retractGantt" > {{ '收起甘特图' }} </el-button> </el-form-item> </el-form> <Dialog title="查看治理计划" :visible.sync="open" width="850px" height="600px" append-to-body> <el-form ref="form" :model="form" label-width="100px" disabled> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="code"> <el-input v-model="form.code" placeholder="请输入编号"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="text"> <el-input v-model="form.text" placeholder="请输入名称"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="上级计划" prop="parent"> <treeselect v-model="form.parent" :options="planOptions" :normalizer="normalizer" placeholder="选择上级计划" disabled /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="完成度(%)" prop="progress"> <el-input-number v-model="form.progress" placeholder="请输入完成百分比"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="责任人" prop="respPerson"> 责任人 {{ form.respPerson }} <el-button size="mini" type="primary" icon="el-icon-user" class="btn-wizard-trigger" style="margin-left: 10px" >选择用户 </el-button> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="责任部门" prop="respDept"> <el-input v-model="form.respDept" placeholder="请输入责任部门"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划开始日期" prop="planStartDate"> <el-date-picker clearable v-model="form.planStartDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择计划开始日期"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际开始日期" prop="realStartDate"> <el-date-picker clearable v-model="form.realStartDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择实际开始日期"> </el-date-picker> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划结束时间" prop="planEndDate"> <el-date-picker clearable v-model="form.planEndDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择计划结束时间"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际结束日期" prop="realEndDate"> <el-date-picker clearable v-model="form.realEndDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择实际结束日期"> </el-date-picker> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划工期(天)" prop="planDuration"> <el-input v-model="form.planDuration" placeholder="请选择计划日期"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际工期(天)" prop="realDuration"> <el-input v-model="form.realDuration" placeholder="请选择实际日期"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" type="textarea" placeholder="请输入备注"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="反馈内容" prop="feedback"> <el-input v-model="form.feedback" type="textarea" placeholder="请输入内容"/> </el-form-item> </el-col> </el-row> </el-form> <el-button @click="open = false">关 闭</el-button> </Dialog> </template> <script> import TablePane from '@/components/TablePane' import Dialog from '@/components/Dialog' import Treeselect from '@riophae/vue-treeselect' import '@riophae/vue-treeselect/dist/vue-treeselect.css' import {gantt} from "dhtmlx-gantt"; import "dhtmlx-gantt/codebase/dhtmlxgantt.css"; import {listPlan} from "@/api/dw/plan/planview"; export default { components: {TablePane, Dialog, Treeselect}, name: "gantt", data() { return { getListFun: listPlan, searchFields: [ {'field': 'name', 'label': '名称', 'type': 'input'}, {'field': 'respPerson', 'label': '责任人', 'type': 'input'}, ], operations: [ { 'label': '展开甘特图', 'click': this.expandGantt, 'hasPermi': ['dw:planview:list'] }], tableData: { 'idField': 'uid', //加载数据后进行转换用于特殊处理 'loadDataHook': this.loadDataHook, 'attrs': { 'row-key': 'uid', '@row-click': this.rowClick, 'tree-props': { children: 'children', hasChildren: 'hasChildren' }, 'default-expand-all': true }, 'column': [ {'label': '编号', 'prop': 'code', 'attrs': { 'sortable': false,'align': 'left' } }, {'label': '状态', 'prop': 'status1', 'attrs': { 'sortable': false } ,'width': 80,'columnHook': this.light}, {'label': '名称', 'prop': 'name', 'attrs': { 'sortable': false } }, {'label': '完成百分比', 'prop': 'schedule', 'attrs': { 'sortable': false } }, {'label': '责任人', 'prop': 'respPerson', 'attrs': { 'sortable': false } }, {'label': '责任部门', 'prop': 'respDept' , 'attrs': { 'sortable': false }}, {'label': '计划开始日期', 'prop': 'planStartDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '计划结束时间', 'prop': 'planEndDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '实际开始日期', 'prop': 'realStartDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '实际结束日期', 'prop': 'realEndDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '计划工期', 'prop': 'planDuration' }, {'label': '实际工期', 'prop': 'realDuration' }, {'label': '备注', 'prop': 'remarks' }, { 'width': 100, 'label': '操作', 'type': 'button', 'attrs': { 'sortable': false, 'fixed': 'right' }, 'buttons': [ { 'icon': 'el-icon-view', 'label': '查看', 'hasPermi': ['dw:planview:query'], 'click': this.handleView }, ] } ] }, /***************************************************甘特图 start****************************************************************/ tasks: { data: [], }, queryParams: { name: null, respPerson: null }, showGantt: true, // 状态控制甘特图显示 planOptions: [], open: false, // 控制详情弹窗显示 form: {} // 当前查看的任务 /***************************************************甘特图 end****************************************************************/ }; }, // 把携带的参数放到queryParams查询参数里 created() { // console.log(this.$route.params.id); }, methods: { /** 查询治理计划列表 */ getList() { this.$refs.tablePane.loadData() }, loadDataHook(response) { response.data = this.handleTree(response.data, 'uid','parentUid') return response }, light(column, row){ const currentDate = this.getCurrentDate(); if( row.realStartDate != null){//有实际开始时间 // 状态灯:绿色:已完成; // 红色:计划开始时间小于当前日期并且没有实际开始日期; // 黄色:有实际开始日期,但是没有实际结束日期,并且计划结束日期小宇当前日期。 // 其他状态都没有颜色(数据体系里面的计划状态灯昨天没写,也按这个规则) var res = this.compareDate(new Date(row.planEndDate),new Date(currentDate)) if( row.realEndDate == null){ if(res== -1){ column.class='circle-light-yellow' } }else{ column.class='circle-light-green' } }else{//没用实际开始时间 红色:没有实际开始,但是当前日期大于计划开始日期; if(row.planStartDate != null){//有计划开始时间 var res = this.compareDate(new Date(row.planStartDate),new Date(currentDate)) if(res == -1){ column.class='circle-light-red' } } } }, getCurrentDate(){ const dateObj = new Date(); const year = dateObj.getFullYear(); // 获取当前年份 const month = ("0" + (dateObj.getMonth() + 1)).slice(-2); // 获取当前月份,其中需要将月份加1,因为月份是从0开始计数的 const day = ("0" + dateObj.getDate()).slice(-2); // 获取当前日期 const formattedDate = ${year}-${month}-${day}; // 格式化日期 return formattedDate; // 输出当前时间的年月日 }, compareDate(date1,date2){//date1 > date2 返回1;date1 < date2 返回-1 相等返回0 if (date1.getTime() > date2.getTime()) { return 1; } else if (date1.getTime() < date2.getTime()) { return -1; } else { return 0; } }, // 上级节点 getTreeselect() { listPlan().then(response => { const data = {uid: 0, name: '顶级节点', children: []}; data.children = this.handleTree(response.data, 'uid', 'parentUid') this.planOptions.push(data) }) }, normalizer(node) { if (node.children && !node.children.length) { delete node.children } return { id: node.uid, label: node.name, children: node.children } }, rowClick(row, column, event) { // 行点击事件 }, /***************************************************甘特图 start****************************************************************/ // 查看任务详情 handleView(taskId) { // 根据任务ID查找任务详情 const task = this.tasks.data.find(item => item.id == taskId); if (task) { this.getTreeselect(); this.form = task; this.open = true; } }, expandGantt() { this.showGantt = false this.$nextTick(() => { this.initGantt(); // 确保DOM更新后再初始化甘特图 }); }, retractGantt() { this.showGantt = true; this.destroyGantt(); // 添加销毁甘特图方法 }, //开始时间-结束时间参数 DateDifference: function (strDateStart, strDateEnd) { var begintime_ms = Date.parse(new Date(strDateStart.replace(/-/g, "/"))); //begintime 为开始时间 var endtime_ms = Date.parse(new Date(strDateEnd.replace(/-/g, "/"))); // endtime 为结束时间 var date3 = endtime_ms - begintime_ms; //时间差的毫秒数 var days = Math.floor(date3 / (24 * 3600 * 1000)); return days; }, // 重置查询 resetQuery() { this.queryParams = { name: null, respPerson: null }; this.initGantt(); }, initGantt: function () { this.destroyGantt(); // 先销毁旧实例 // 确保容器存在 if (!this.$refs.gantt) return; // 设置容器尺寸 this.$refs.gantt.style.height = ${window.innerHeight - 150}px; // 在初始化前配置所有设置 //自适应甘特图的尺寸大小, 使得在不出现滚动条的情况下, 显示全部任务 gantt.config.autosize = true; //只读模式 gantt.config.readonly = true; //是否显示左侧树表格 gantt.config.show_grid = true; //时间轴配置 gantt.config.show_task_cells = true; //当task的长度改变时,自动调整图表坐标轴区间用于适配task的长度 gantt.config.fit_tasks = true; gantt.config.min_column_width = 50; gantt.config.auto_types = true; gantt.config.xml_date = "%Y-%m-%d"; gantt.config.scale_unit = "month"; gantt.config.step = 1; gantt.config.date_scale = "%Y年%M"; gantt.config.start_on_monday = true; gantt.config.scale_height = 160; gantt.config.autoscroll = true; gantt.config.calendar_property = "start_date"; gantt.config.calendar_property = "end_date"; gantt.config.readonly = true; //时间刻度配置 var weekScaleTemplate = function (date) { var dateToStr = gantt.date.date_to_str("%m %d"); var endDate = gantt.date.add( gantt.date.add(date, 1, "week"), -1, "day" ); var weekNum = gantt.date.date_to_str("第 %W 周"); return weekNum(date); }; gantt.config.subscales = [ { unit: "week", step: 1, template: weekScaleTemplate, }, { unit: "day", step: 1, format: "%d", }, ]; //表格列设置 gantt.config.columns = [ { name: "code", label: "编号", tree: true, width: "160", onrender: function (task, node) { node.setAttribute( "class", "gantt_cell gantt_last_cell gantt_cell_tree " + task.status ); }, }, { name: "status", label: "状态", align: "center", width: "80", template: function (task) { // 自定义状态列显示为状态灯 return ; } }, {name: "text", label: "名称", align: "center", width: "180", hide: true}, {name: "progress", label: "完成度(%)", align: "center", width: "90", hide: true}, {name: "respPerson", label: "责任人", align: "center", width: "120", hide: true}, {name: "respDept", label: "责任部门", align: "center", width: "140", hide: true}, {name: "planStartDate", label: "计划开始日期", align: "center", width: "130", hide: true}, {name: "planEndDate", label: "计划结束时间", align: "center", width: "130", hide: true}, {name: "realStartDate", label: "实际开始日期", align: "center", width: "130", hide: true}, {name: "realEndDate", label: "实际结束日期", align: "center", width: "130", hide: true}, {name: "planDuration", label: "计划工期", align: "center", width: "90", hide: true}, {name: "realDuration", label: "实际工期", align: "center", width: "90", hide: true}, {name: "remarks", label: "备注", align: "center", width: "220", hide: true}, ]; // 初始化 gantt.init(this.$refs.gantt); gantt.plugins({ tooltip: true, }); //设置鼠标放置显示事件 gantt.attachEvent("onGanttReady", function() { var tooltips = gantt.ext.tooltips; gantt.templates.tooltip_text = function(start, end, task) { return "编号:" + task.code + "
" + "名称:" + task.text + "
" + "计划开始:" + gantt.templates.tooltip_date_format(start) + "
" + "工期:" + task.duration }; }); //设置任务条进度内容 gantt.templates.progress_text = function (start, end, task) { return ( "" + task.progress + "% " ); }; //任务条显示内容 gantt.templates.task_text = function (start, end, task) { return ( "" + task.text + "(" + task.duration + "天)" + "" ); }; //任务条上的文字大小 以及取消border自带样式 gantt.templates.task_class = function (start, end, item) { return item.$level == 0 ? "firstLevelTask" : "secondLevelTask"; }; // 设置布局 gantt.config.layout = { css: "gantt_container", cols: [ { width: "800", // 左侧表格固定宽度 min_width: 300, rows: [ { view: "grid", scrollX: "gridScroll", scrollable: true, scrollY: "scrollVer", }, { view: "scrollbar", id: "gridScroll", group: "horizontal", }, ], }, { resizer: true, width: 1, }, { rows: [ { view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer", }, { view: "scrollbar", id: "scrollHor", group: "horizontal", }, ] } ], }; gantt.i18n.setLocale("cn"); listPlan(this.queryParams).then((res) => { this.tasks.data = res.data.map((item) => { let statusColor; //存在status字段 说明非一级菜单,判断阶段的具体类型 设置不同颜色 if (item.status == '1') { //冒烟 statusColor = "#84bd54" } else if (item.status == '2') { //单元 statusColor = "#fcca02" } else if (item.status == '3') { //回归 statusColor = "#dc1626" } else { statusColor = "#999999" } return { id: item.uid, parent: item.parent, text: item.name, start_date: item.planStartDate, duration: item.planDuration, open: true, //默认打开, toolTipsTxt: item.name, progress: item.schedule, status: item.status, code: item.code, respPerson: item.respPerson, respDept: item.respDept, planStartDate: item.planStartDate, planEndDate: item.planEndDate, realStartDate: item.realStartDate, realEndDate: item.realEndDate, planDuration: item.planDuration, realDuration: item.realDuration, remarks: item.remarks, feedback: item.feedback, color: statusColor, } }); // 数据解析 gantt.parse(this.tasks); }); // 添加双击行事件监听器 gantt.attachEvent("onTaskDblClick", function(id, e) { // 调用查看详情方法 window.vueInstance.handleView(id); return true; }); }, destroyGantt() { // 推荐方式:检查容器是否有内容 if (this.$refs.gantt && this.$refs.gantt.children.length > 0) { gantt.destroy(); // 安全销毁 } this.$refs.gantt.innerHTML = ""; // 清空容器 }, }, mounted() { window.vueInstance = this; if (!this.showGantt) { this.$nextTick(this.initGantt); } else { this.getList(); } }, }; </script> <style lang="scss" scoped> .firstLevelTask { border: none; .gantt_task_content { font-size: 13px; } } .secondLevelTask { border: none; } .thirdLevelTask { border: 2px solid #da645d; color: #da645d; background: #da645d; } .milestone-default { border: none; background: rgba(0, 0, 0, 0.45); } .milestone-unfinished { border: none; background: #5692f0; } .milestone-finished { border: none; background: #84bd54; } .milestone-canceled { border: none; background: #da645d; } html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; } .container { height: 100%; width: 100%; position: relative; padding: 10px; .gantt_grid_head_cell { padding-left: 20px; text-align: left !important; font-size: 14px; color: #333; } .left-container { height: 100%; } .green, .yellow, .pink, .popular { .gantt_tree_icon.gantt_file { background: none; position: relative; &::before { content: ""; width: 10px; height: 10px; border-radius: 50%; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } } } .green { .gantt_tree_icon.gantt_file { &::before { background: #84bd54; } } } .yellow { .gantt_tree_icon.gantt_file { &::before { background: #fcca02; } } } .pink { .gantt_tree_icon.gantt_file { &::before { background: #da645d; } } } .popular { .gantt_tree_icon.gantt_file { &::before { background: #d1a6ff; } } } } .left-container { height: 100%; } .gantt_task_content { text-align: left; padding-left: 10px; } .gantt-container { height: calc(100vh - 150px) !important; width: 100%; } // 状态灯样式 ::v-deep .gantt_grid_data .gantt_cell div.status-light { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin: 0 auto; } // 表格表头居中样式 ::v-deep .gantt_grid_head_cell { text-align: center !important; } .search-wrapper { text-align: left; padding: 10px 0; } .search-from { .el-form-item--mini.el-form-item, .el-form-item--small.el-form-item { margin-bottom: 3px; } } </style> <style lang="less"> .circle-light-yellow > span::before { content: ''; display: inline-block; width: 16px; height: 16px; background-color: #e6a23c; border-radius: 50%; margin-right: 10px; } .circle-light-green > span::before { content: ""; display: inline-block; width: 16px; height: 16px; background-color: #67c23a; border-radius: 50%; margin-right: 10px; } .circle-light-red > span::before { content: ""; display: inline-block; width: 16px; height: 16px; background-color: #f56c6c; border-radius: 50%; margin-right: 10px; } </style> 第一次展开甘特图时表格铺满了,请修复

<template> <el-row :gutter="20"> <el-col :span="24"> <el-form :inline="true" label-width="100px" @submit.prevent="getList"> <el-form-item label="名称"> <el-input v-model="queryParams.name" placeholder="请输入名称" /> </el-form-item> <el-form-item label="责任人"> <el-input v-model="queryParams.respPerson" placeholder="请输入责任人" /> </el-form-item> <el-form-item> <el-button type="primary" @click="getList">查询</el-button> <el-button @click="resetQuery">重置</el-button> </el-form-item> </el-form> </el-col> <el-col :span="24"> <el-button type="primary" @click="toggleGantt" style="margin-bottom: 15px;" > {{ showGantt ? '收起甘特图' : '展开甘特图' }} </el-button> </el-col> <el-col :span="showGantt ? 12 : 24"> <el-table ref="table" :data="listData" row-key="uid" border :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" @row-click="rowClick" @expand-change="handleExpandChange" > <el-table-column prop="code" label="编号" /> <el-table-column prop="name" label="名称" /> <el-table-column prop="respPerson" label="责任人" /> <el-table-column prop="schedule" label="完成百分比" /> <el-table-column prop="planStartDate" label="计划开始日期" /> <el-table-column prop="planEndDate" label="计划结束日期" /> <el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" icon="el-icon-view" @click="handleUpdate(scope.row)">查看</el-button> </template> </el-table-column> </el-table> <el-pagination layout="prev, pager, next" :total="total" :page-size="queryParams.pageSize" @current-change="handleCurrentChange" /> </el-col> <el-col :span="12" v-if="showGantt"> </el-col> </el-row> <el-dialog :title="title" :visible.sync="open" width="850px" append-to-body> <el-form ref="form" :model="form" :rules="rules" label-width="100px" :disabled="disable"> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="code"> <el-input v-model="form.code" placeholder="请输入编号" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="name"> <el-input v-model="form.name" placeholder="请输入名称" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" type="textarea" placeholder="请输入备注" /> </el-form-item> </el-col> </el-row> <el-button @click="cancel">取 消</el-button> </el-form> </el-dialog> </template> <script> import gantt from 'dhtmlx-gantt'; import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'; import { listPlan, getPlan } from '@/api/dw/plan/planview'; export default { name: 'Planview', data() { return { listData: [], total: 0, queryParams: { pageNum: 1, pageSize: 10, name: null, respPerson: null }, open: false, title: '', form: {}, rules: { name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], schedule: [ { required: true, message: '完成百分比不能为空', trigger: 'blur' }, { type: 'number', message: '输入内容不是有效的数字', trigger: 'blur' } ] }, disable: true, showGantt: true, // 控制甘特图显示 flatData: [], // 扁平化数据 baseDate: new Date('2023-01-01'), // 基准日期 maxDuration: 365, // 最大工期(天) maxOffset: 365 // 最大偏移天数 }; }, mounted() { this.getList(); this.initGantt(); }, methods: { async getList() { const res = await listPlan(this.queryParams); this.listData = this.handleTree(res.data, 'uid', 'parentUid'); this.total = res.total; this.flatData = this.flattenTree(this.listData); this.getMaxDuration(); this.$nextTick(() => { const tasks = this.ganttData(this.flatData); this.updateGantt(tasks); }); }, // 获取最大工期 getMaxDuration() { const durations = this.flatData.map(item => item.planDuration || 0); this.maxDuration = Math.max(...durations, 1); }, // 计算甘特图宽度 calculateGanttWidth(row) { const duration = row.planDuration || 0; const width = (duration / this.maxDuration) * 100; return ${Math.max(5, width)}%; }, // 计算甘特图偏移 calculateGanttOffset(row) { if (!row.planStartDate) return '0%'; const startDate = new Date(row.planStartDate); const daysOffset = Math.floor((startDate - this.baseDate) / (1000 * 60 * 60 * 24)); return ${(daysOffset / this.maxOffset) * 100}%; }, // 初始化甘特图 initGantt() { if (!this.$refs.ganttContainer) return; gantt.config.date_format = '%Y-%m-%d'; gantt.config.columns = [ { name: 'text', label: '任务名称', tree: true, width: '*' }, { name: 'start_date', label: '开始时间', align: 'center' }, { name: 'duration', label: '工期(天)', align: 'center' } ]; gantt.templates.task_text = (start, end, task) => task.text; gantt.init(this.$refs.ganttContainer); gantt.parse({ data: [], links: [] }); // 确保事件监听器只绑定一次 if (!this.ganttEventInitialized) { gantt.attachEvent('onTaskSelected', id => { const row = this.flatData.find(item => item.uid === id); if (row) { this.$refs.table.setCurrentRow(row); } }); this.ganttEventInitialized = true; } }, // 更新甘特图 updateGantt(tasks) { gantt.clearAll(); gantt.parse({ data: tasks, links: [] }); }, // 树形结构转扁平结构 flattenTree(data) { const result = []; const stack = [...data]; while (stack.length) { const node = stack.pop(); result.push(node); if (node.children) { stack.push(...node.children); } } return result; }, // 转换为甘特图数据 ganttData(data) { return data .filter(item => item.uid && item.planStartDate) .map(item => ({ id: item.uid, text: item.name, start_date: item.planStartDate, duration: item.planDuration || 0, progress: (item.schedule || 0) / 100, parent: item.parentUid || 0 })); }, // 处理树形结构 handleTree(data, idKey = 'id', parentKey = 'parentId') { const map = {}; const tree = []; data.forEach(item => (map[item[idKey]] = item)); data.forEach(item => { const parent = map[item[parentKey]]; if (parent) { (parent.children || (parent.children = [])).push(item); } else { tree.push(item); } }); return tree; }, // 行点击事件 rowClick(row) { const taskId = row.uid; this.$nextTick(() => { if (gantt.$initialized) { gantt.showTask(taskId); gantt.selectTask(taskId); // 强制重绘确保高亮生效 gantt.render(); } }); }, // 树展开/折叠更新甘特图 handleExpandChange(row, expanded) { if (expanded) { const allChildren = this.getAllChildren(row); const tasks = this.ganttData(allChildren); this.$nextTick(() => { this.updateGantt(tasks); if (gantt.$initialized && tasks.length > 0) { // 获取展开节点的最早和最晚日期 const dates = tasks .filter(t => t.start_date) .map(t => new Date(t.start_date)); if (dates.length > 0) { const minDate = new Date(Math.min(...dates.map(d => d.getTime()))); const maxDate = new Date(Math.max(...dates.map(d => { const endDate = new Date(t.start_date); endDate.setDate(endDate.getDate() + (t.duration || 0)); return endDate.getTime(); }))); // 设置视图时间范围 gantt.setWorkTime({ start_date: minDate, end_date: maxDate }); // 调整视图缩放级别 gantt.config.scale_unit = 'day'; gantt.config.step = 1; gantt.config.scale_height = 28; // 重新渲染并定位第一个任务 gantt.render(); gantt.showTask(tasks[0].id); gantt.selectTask(tasks[0].id); } } }); } else { const topLevelTasks = this.listData.map(item => ({ id: item.uid, text: item.name, start_date: item.planStartDate, duration: item.planDuration || 0, progress: (item.schedule || 0) / 100, parent: item.parentUid || 0 })); this.$nextTick(() => { this.updateGantt(topLevelTasks); if (gantt.$initialized) { // 恢复默认时间范围 gantt.setWorkTime({ start_date: new Date('2023-01-01'), end_date: new Date('2023-12-31') }); gantt.config.scale_unit = 'month'; gantt.config.step = 1; gantt.config.scale_height = 28; gantt.render(); } }); } }, // 递归获取所有子节点 getAllChildren(node) { let children = [node]; if (node.children && node.children.length > 0) { node.children.forEach(child => { children = children.concat(this.getAllChildren(child)); }); } return children; }, // 切换甘特图 toggleGantt() { this.showGantt = !this.showGantt; if (this.showGantt) { this.$nextTick(() => { const tasks = this.ganttData(this.flatData); this.updateGantt(tasks); }); } }, // 获取数据 async handleUpdate(row) { const res = await getPlan(row.uid); this.form = res.data; this.open = true; this.title = '查看治理计划'; }, // 取消按钮 cancel() { this.open = false; }, // 重置查询 resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, name: null, respPerson: null }; this.getList(); }, // 分页切换 handleCurrentChange(page) { this.queryParams.pageNum = page; this.getList(); } } }; </script> <style scoped> .page-container { padding: 20px; } .table-container { background-color: #fff; padding: 10px; border-radius: 4px; } .gantt-container { background-color: #f9f9f9; border: 1px solid #ebeef5; padding: 10px; border-radius: 4px; } .dialog-footer { text-align: right; } .search-wrapper { margin-bottom: 20px; background-color: #fff; padding: 10px; border-radius: 4px; } .toggle-button { margin-bottom: 15px; } .gantt-bar-container { position: relative; height: 30px; background-color: #f5f7fa; border-radius: 4px; overflow: hidden; margin: 5px 0; } .gantt-bar { position: absolute; top: 0; left: 0; height: 100%; background-color: #409EFF; color: white; text-align: center; font-size: 12px; line-height: 30px; } </style> 列表的展开和收缩,甘特图没有同步,请重新优化

<template> 0" class="tabs-wrapper"> {{ section.name }} <a-empty description="暂无分类" /> <a-form :model="searchForm" label-align="left" auto-label-width layout="inline" :size="'large'" class="search-header" ref="searchHeaderRef"> <a-form-item style="width: 320px;" field="base_title" label="能力名称:" size="large"> <a-input allow-clear v-model="searchForm.base_title" placeholder="搜索能力..." @press-enter="fetchTableData" @clear="resetSearch"> <template #prefix> <icon-search /> </template> </a-input> </a-form-item> <a-form-item> <a-space size='large'> <a-button type="primary" size="medium" @click="fetchTableData"> <template #icon><icon-search /></template>查询 </a-button> <a-button type="outline" size="medium" @click="resetSearch"> <template #icon><icon-refresh /></template>重置 </a-button> </a-space> </a-form-item> </a-form> <a-spin :loading="loading" style="width: 100%"> 0" class="section-container" ref="sectionContainerRef" @scroll="handleScroll"> <icon-apps /> {{ section.name }} <template v-for="(subSection, subIndex) in section.children" :key="subIndex"> {{ subSection.name }} 能力背景 能力图标 <a-image width="100" height="100" src="some-error.png" /> {{ item.title }} {{ item.description }} </template> <a-empty description="暂无能力数据" /> </a-spin> </template> <script setup lang="ts"> import { ref, onMounted, nextTick, reactive } from 'vue'; import { IconSearch, IconApps, IconRefresh } from '@arco-design/web-vue/es/icon'; import { Message } from '@arco-design/web-vue'; import { useAbilityMallStore } from '@/store/modules/ability-mall'; import { storeToRefs } from 'pinia'; import { useRouter } from 'vue-router'; import { getAbilityMallList, getabilityMallDetails, createAbilityMall, } from '@/api/abilityMall'; const abilityMallStore = useAbilityMallStore(); const { abilityMallList, abilityMallDetails } = storeToRefs(abilityMallStore); const { getabilityMallDetailsStore } = abilityMallStore; const router = useRouter(); // 定义三级分类数据结构 interface CategoryItem { id: string; title: string; description: string; image: string; } interface SubCategory { id: string; name: string; children: CategoryItem[]; } interface MainCategory { id: string; name: string; children: SubCategory[]; } // 状态管理 const sections = ref<MainCategory[]>([]); const sectionContainerRef = ref<HTMLElement | null>(null); const searchHeaderRef = ref<HTMLElement | null>(null); // 添加搜索表单引用 const sectionRefs = ref<HTMLElement[]>([]); const activeIndex = ref(0); const loading = ref(false); const headerHeight = ref(0); const searchForm = reactive({ base_title: '', }); // 更高效的数据转换函数 const transformData = (apiData: any[]): MainCategory[] => { const categoryMap = new Map<string, MainCategory>(); const subcategoryMap = new Map<string, SubCategory>(); apiData.forEach((item) => { const categoryName = item.base_category; const categoryId = categoryName.replace(/\s+/g, '-').toLowerCase(); // 处理一级分类 if (!categoryMap.has(categoryId)) { categoryMap.set(categoryId, { id: categoryId, name: categoryName, children: [], }); } const category = categoryMap.get(categoryId)!; // 处理二级分类 const subcategoryName = item.base_subcategory; const subcategoryId = ${categoryId}-${subcategoryName .replace(/\s+/g, '-') .toLowerCase()}; if (!subcategoryMap.has(subcategoryId)) { const subcategory: SubCategory = { id: subcategoryId, name: subcategoryName, children: [], }; subcategoryMap.set(subcategoryId, subcategory); category.children.push(subcategory); } const subcategory = subcategoryMap.get(subcategoryId)!; // 添加三级分类项 subcategory.children.push({ id: item.id, title: item.base_title, description: item.base_content, image: item.base_image, // 添加图片字段 }); }); return Array.from(categoryMap.values()); }; // 设置章节引用 const setSectionRef = (el: any) => { if (el) { sectionRefs.value.push(el); } }; // 获取搜索表单高度 const getSearchHeaderHeight = (): number => { return searchHeaderRef.value?.offsetHeight || 80; }; // 滚动到指定章节 - 修复滚动位置计算 const scrollToSection = (index: number) => { if (!sectionContainerRef.value || index < 0 || index >= sectionRefs.value.length) return; activeIndex.value = index; const targetSection = sectionRefs.value[index]; const headerHeight = getSearchHeaderHeight(); // 精确计算滚动位置:目标位置 - 顶部间距 + 容器滚动位置 const targetOffset = targetSection.offsetTop - headerHeight; sectionContainerRef.value.scrollTo({ top: targetOffset, behavior: 'smooth', }); }; // 初始化滚动监听 const initScrollListener = () => { if (!sectionContainerRef.value) return; // 初始计算一次搜索表单高度 headerHeight.value = getSearchHeaderHeight(); }; // 处理滚动事件 - 修复滚动位置判断逻辑 const handleScroll = () => { if (!sectionContainerRef.value || sectionRefs.value.length === 0) return; const scrollTop = sectionContainerRef.value.scrollTop; const containerHeight = sectionContainerRef.value.clientHeight; const headerHeight = getSearchHeaderHeight(); // 计算有效滚动位置(考虑搜索表单高度) const effectiveScrollTop = scrollTop + headerHeight; // 1. 处理滚动到底部的情况 if (scrollTop + containerHeight >= sectionContainerRef.value.scrollHeight - 10) { activeIndex.value = sectionRefs.value.length - 1; return; } // 2. 精确计算当前激活的section let currentIndex = 0; let closestDistance = Number.MAX_VALUE; // 找到距离顶部最近的section for (let i = 0; i < sectionRefs.value.length; i++) { const section = sectionRefs.value[i]; const distance = Math.abs(section.offsetTop - effectiveScrollTop); if (distance < closestDistance) { closestDistance = distance; currentIndex = i; } } // 3. 处理最后两个section的特殊情况 if (sectionRefs.value.length > 1) { const lastSection = sectionRefs.value[sectionRefs.value.length - 1]; const secondLastSection = sectionRefs.value[sectionRefs.value.length - 2]; // 当滚动位置接近最后两个section时 if (effectiveScrollTop >= secondLastSection.offsetTop - 50) { // 如果已经滚动到最后一个section if (effectiveScrollTop >= lastSection.offsetTop - headerHeight) { currentIndex = sectionRefs.value.length - 1; } else { currentIndex = sectionRefs.value.length - 2; } } } // 只有当索引变化时才更新 if (activeIndex.value !== currentIndex) { activeIndex.value = currentIndex; } }; // 重置搜索 const resetSearch = () => { searchForm.base_title = ''; fetchTableData(); }; // 获取数据 async function fetchTableData() { loading.value = true; try { const { data } = await getAbilityMallList(searchForm); const fakeData = data.filter((item) => item.deleted === 0); sections.value = transformData(fakeData || []); // 重置引用 nextTick(() => { sectionRefs.value = []; // 初始化滚动监听 setTimeout(() => { initScrollListener(); handleScroll(); // 初始计算一次激活状态 // 添加初始滚动位置修正 if (sections.value.length > 0) { scrollToSection(0); } }, 100); }); } catch (error) { console.error('获取能力模型失败:', error); Message.error('获取能力模型失败'); } finally { loading.value = false; } } // 跳转详情 const sceneDetail = async (item: CategoryItem) => { loading.value = true; await getabilityMallDetailsStore(item.id); router.push({ name: 'AbilityDetails', query: { id: item.id }, }); loading.value = false; }; // 生命周期钩子 onMounted(() => { fetchTableData(); }); </script> <style scoped> .container { display: flex; height: 100vh; position: relative; padding: 0; background: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } /* 左侧导航栏样式 */ .tabs { width: 200px; padding: 20px 0; flex-shrink: 0; position: sticky; top: 0; max-height: 100vh; overflow-y: auto; z-index: 10; background: #f9fafb; border-right: 1px solid #e5e6eb; .tabs-wrapper { padding: 0 10px; } .tab { height: 44px; line-height: 44px; text-align: center; margin: 8px 0; font-size: 15px; color: #1d2129; cursor: pointer; position: relative; transition: all 0.3s ease; border-radius: 6px; overflow: hidden; font-weight: 500; &:hover { background: #f2f3f5; color: #3261CE; transform: translateX(3px); } } .tab.active { color: #3261CE; background: #e8f3ff; font-weight: 600; box-shadow: 0 1px 4px rgba(22, 93, 255, 0.15); &::before { content: ''; position: absolute; left: 0; top: 0; height: 100%; width: 3px; background: #3261CE; } } .empty-container { padding: 40px 20px; } } /* 右侧内容区域 */ .content { flex: 1; height: 100%; position: relative; padding: 0; overflow: hidden; display: flex; flex-direction: column; } /* 搜索头部样式 */ .search-header { width: 100%; display: flex; gap: 16px; padding: 15px 24px; background: #fff; border-bottom: 1px solid #e5e6eb; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03); position: sticky; top: 0; z-index: 20; .header-title { font-size: 20px; font-weight: 600; margin-right: 20px; color: #1d2129; } } /* 分类区域样式 - 关键修改 */ .section-container { flex: 1; overflow-y: auto; scroll-behavior: smooth; padding: 0 24px; position: relative; height: calc(100vh - 80px); /* 减去搜索栏高度 */ } .section { padding: 32px 0; border-bottom: 1px solid #e5e6eb; &:first-child { padding-top: 24px; } &:last-child { border-bottom: none; } } .section-title { font-size: 22px; color: #1d2129; padding-bottom: 16px; margin-bottom: 16px; border-bottom: 1px solid #e5e6eb; font-weight: 600; display: flex; align-items: center; .arco-icon { margin-right: 10px; color: #3261CE; font-size: 20px; } } .title { font-size: 18px; color: #3261CE; margin: 24px 0 16px; padding-left: 12px; border-left: 4px solid #3261CE; font-weight: 500; } /* 内容项网格布局 */ .sub-content { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; margin-top: 8px; } /* 内容项样式 */ .content-item { position: relative; height: 95px; cursor: pointer; border-radius: 8px; overflow: hidden; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.03); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); background: #fff; border: 1px solid #e5e6eb; /* 背景图片容器 */ .img-wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; opacity: 0; transform: scale(1.05); transition: all 0.5s cubic-bezier(0.22, 0.61, 0.36, 1); img { width: 100%; height: 100%; object-fit: cover; filter: blur(0); transition: filter 0.5s ease; } } /* 悬停效果 */ &:hover { transform: translateY(-5px); box-shadow: 0 12px 25px rgba(22, 93, 255, 0.2); border-color: rgba(22, 93, 255, 0.3); .img-wrapper { opacity: 1; transform: scale(1); img { filter: blur(4px) brightness(0.9); } } .item { background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(4px); } .item-image img { transform: scale(1.8); } } } /* 内容卡片样式 */ .item { display: flex; position: relative; overflow: hidden; border-radius: 8px; width: 100%; height: 100%; z-index: 2; background: rgba(255, 255, 255, 0.95); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } /* 图片样式 */ .item-image { width: 100px; height: 100%; flex-shrink: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #f7f8fa; transition: background 0.3s ease; img { width: 60px; height: 60px; object-fit: contain; display: block; transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1); } .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #c9cdd4; .arco-icon { font-size: 32px; } } } /* 文本区域样式 */ .item-text { flex: 1; padding: 16px 12px; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } .item-title { font-size: 16px; color: #1d2129; font-weight: 500; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 0.3s ease; } .item-desc { font-size: 13px; color: #86909c; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; line-height: 1.5; transition: color 0.3s ease; } /* 响应式调整 */ @media (max-width: 992px) { .container { flex-direction: column; } .tabs { width: 100%; position: sticky; top: 0; z-index: 30; display: flex; overflow-x: auto; padding: 10px 0; height: auto; border-right: none; border-bottom: 1px solid #e5e6eb; .tabs-wrapper { display: flex; padding: 0 16px; } .tab { flex-shrink: 0; margin: 0 8px; padding: 0 16px; height: 36px; line-height: 36px; } } .content { margin-left: 0; } .search-header { padding: 12px 16px; flex-wrap: wrap; } .section-container { padding: 0 16px; height: calc(100vh - 140px); /* 调整移动端高度 */ } .section { padding: 24px 0; } } @media (max-width: 576px) { .sub-content { grid-template-columns: 1fr; } .header-title { display: none; } .search-header { :deep(.arco-form-item) { width: 100%; margin-bottom: 12px; } } .section-container { height: calc(100vh - 180px); /* 调整小屏幕高度 */ } .content-item { height: 110px; } .item-image { width: 80px; } } </style>在这个代码基础上修改

<template> 0" class="tabs-wrapper"> {{ section.name }} <a-empty /> <a-form ref="searchHeaderRef" :model="searchForm" label-align="left" auto-label-width layout="inline" :size="'large'" class="search-header" > <a-form-item field="base_title" label="能力名称:" size="large" style="width: 420px" > <a-input allow-clear v-model="searchForm.base_title" placeholder="请输入能力名称..." @press-enter="fetchTableData" @clear="resetSearch" /> </a-form-item> <a-form-item field="" label=""> <a-space size="medium"> <a-button type="primary" size="medium" @click="fetchTableData" >查询</a-button > <a-button type="primary" size="medium" @click="resetSearch" >刷新</a-button > </a-space> </a-form-item> </a-form> <a-spin :loading="loading" style="width: 100%"> 0"> {{ section.name }} <template v-for="(subSection, subIndex) in section.children" :key="subIndex" > {{ subSection.name }} 背景图片 内容图片 <icon-image /> {{ item.title }} {{ item.description }} </template> <a-empty /> </a-spin> </template> <script setup lang="ts"> import { ref, onMounted, nextTick, reactive, onBeforeUnmount } from 'vue'; import { Message } from '@arco-design/web-vue'; import { IconImage } from '@arco-design/web-vue/es/icon'; import { useAbilityMallStore } from '@/store/modules/ability-mall'; import { storeToRefs } from 'pinia'; import { useRouter } from 'vue-router'; import { getAbilityMallList, getabilityMallDetails, createAbilityMall, } from '@/api/abilityMall'; const abilityMallStore = useAbilityMallStore(); const { abilityMallList, abilityMallDetails } = storeToRefs(abilityMallStore); const { getabilityMallDetailsStore } = abilityMallStore; const router = useRouter(); // 定义三级分类数据结构 interface CategoryItem { id: string; title: string; description: string; image: string; // 添加图片字段 } interface SubCategory { id: string; name: string; children: CategoryItem[]; } interface MainCategory { id: string; name: string; children: SubCategory[]; } // 状态管理 const sections = ref<MainCategory[]>([]); const contentRef = ref<HTMLElement | null>(null); const sectionRefs = ref<HTMLElement[]>([]); const activeIndex = ref(0); const loading = ref(false); const searchForm = reactive({ base_title: '', current: '', pageSize: '', }); // 创建 IntersectionObserver 实例 let observer = ref(null) // 更高效的数据转换函数 - 修复图片字段 const transformData = (apiData: any[]): MainCategory[] => { const categoryMap = new Map<string, MainCategory>(); const subcategoryMap = new Map<string, SubCategory>(); apiData.forEach((item) => { const categoryName = item.base_category; const categoryId = categoryName.replace(/\s+/g, '-').toLowerCase(); // 处理一级分类 if (!categoryMap.has(categoryId)) { categoryMap.set(categoryId, { id: categoryId, name: categoryName, children: [], }); } const category = categoryMap.get(categoryId)!; // 处理二级分类 const subcategoryName = item.base_subcategory; const subcategoryId = ${categoryId}-${subcategoryName .replace(/\s+/g, '-') .toLowerCase()}; if (!subcategoryMap.has(subcategoryId)) { const subcategory: SubCategory = { id: subcategoryId, name: subcategoryName, children: [], }; subcategoryMap.set(subcategoryId, subcategory); category.children.push(subcategory); } const subcategory = subcategoryMap.get(subcategoryId)!; // 添加三级分类项 - 包含图片字段 subcategory.children.push({ id: item.id, title: item.base_title, description: item.base_content, image: item.base_image, // 添加图片字段 }); }); return Array.from(categoryMap.values()); }; // 设置章节引用 const setSectionRef = (el: any) => { if (el) { sectionRefs.value.push(el); } }; // 左侧 Tab 点击跳转 const scrollToSection = (index) => { activeIndex.value = index; const section = sectionRefs.value[index]; if (section) { section.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; // 初始化观察者 const initObserver = () => { observer.value = new IntersectionObserver( (entries) => { entries.forEach((entry) => { const index = sectionRefs.value.indexOf(entry.target); if (entry.isIntersecting) { activeIndex.value = index; } }); }, { threshold: 0.5, // 当目标元素 50% 可见时触发 rootMargin: "0px 0px -100% 0px", // 提前触发,防止底部卡住 } ); // 开始观察所有章节 sectionRefs.value.forEach((section) => { if (section) observer.observe(section); }); }; // 销毁观察者 const destroyObserver = () => { if (observer) { observer.value.disconnect(); } }; onMounted(() => { fetchTableData(); initObserver(); }); onBeforeUnmount(() => { destroyObserver(); }); // 重置搜索 const resetSearch = () => { searchForm.base_title = ''; fetchTableData(); }; // 获取数据 async function fetchTableData() { loading.value = true; try { const { data } = await getAbilityMallList(searchForm); const filteredData = data.filter((item) => item.deleted === 0); // const fakeData = Array.from({ length: 100 }, (_, index) => ({ // id: item-${index}, // base_category: 一级分类 ${Math.floor(Math.random() * 20)}, // base_subcategory: 二级分类 ${Math.floor(Math.random() * 10)}, // base_title: 能力名称 ${index + 1}, // base_content: 描述内容 ${index + 1}, // base_image: https://via.placeholder.com/100?text=Image${index + 1}, // deleted: Math.random() > 0.2 ? 0 : 1, // 80% chance of being valid // })); // 使用模拟数据 sections.value = transformData(filteredData || []); // 重置引用 nextTick(() => { sectionRefs.value = []; // 添加初始滚动位置修正 setTimeout(() => { const hash = window.location.hash.substring(1); if (hash) { const index = sections.value.findIndex((s) => s.id === hash); if (index !== -1) { scrollToSection(index); } } else if (sections.value.length > 0) { scrollToSection(0); } }, 100); }); } catch (error) { console.error('获取能力模型失败:', error); Message.error('获取能力模型失败'); loading.value = false; } finally { loading.value = false; } } // 跳转详情 const sceneDetail = async (item: CategoryItem) => { await getabilityMallDetailsStore(item.id); router.push({ path: '/ability/details', query: { id: item.id }, }); }; </script> <style scoped lang="less"> /* 父容器 */ .container { display: flex; height: 100vh; /* 让父容器撑满页面 */ position: relative; padding-left: 40px; background: #fff; font-family: 'DingTalkJinBuTi', serif; } /* 左侧导航栏样式 */ .tabs { width: 180px; padding: 20px 0; flex-shrink: 0; position: sticky; /* 使用粘性定位 */ top: 0; /* 固定在页面顶部 70px 位置 */ max-height: calc(100vh - 70px); /* 最大高度为视口高度,避免溢出 */ overflow-y: auto; /* 允许左侧滚动条 */ z-index: 10; .tab { height: 40px; line-height: 40px; letter-spacing: 1px; text-align: center; margin: 20px; margin-bottom: 12px; font-size: 16px; color: #181818; cursor: pointer; position: relative; transition: all 0.3s ease-in-out; border-radius: 4px; &:hover { background: linear-gradient(90deg, #3261ce 0%, #739ce2 100%); color: #fff; opacity: 0.9; transform: translateX(5px); } } .tab.active { color: #fff; background: linear-gradient(90deg, #3261ce 0%, #739ce2 100%); opacity: 1; box-shadow: 0 2px 8px rgba(50, 97, 206, 0.4); } } .tab-wrapper { overflow: auto; height: 100%; } /* 右侧内容区域 */ .content { flex: 1; height: 100%; overflow-y: auto; /* 允许右侧内容滚动 */ position: relative; scroll-behavior: smooth; margin-left: 20px; /* 右侧内容区域距离左侧导航栏 */ padding-top: 20px; padding-left: 20px; border-left: 1px solid rgba(0, 0, 0, 0.05); } /* 搜索头部样式 */ .content .search-header { width: 100%; display: flex; gap: 16px; padding: 15px 20px; background: #fff; border-bottom: 1px solid rgba(0, 0, 0, 0.05); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); ::v-deep(.arco-form-item) { margin-bottom: 0; } ::v-deep(.arco-form-item-label) { color: #4e5965; } } /* 分类区域样式 */ .section { height: auto; padding: 20px 20px 20px 0; position: relative; border-bottom: 1px solid rgba(0, 0, 0, 0.05); margin-top: 20px; &:first-child { margin-top: 0; } } .section .section-title { border-bottom: 1px solid rgba(0, 0, 0, 0.1); font-size: 22px; color: #181818; height: 50px; display: flex; align-items: center; padding-bottom: 10px; margin-bottom: 20px; } .section .title { font-size: 18px; color: #3261ce; margin: 24px 0 12px; padding-left: 10px; border-left: 4px solid #3261ce; } /* 内容项网格布局 */ .section .sub-content { display: grid; grid-template-columns: repeat(auto-fill, minmax(242px, 1fr)); gap: 20px; margin-top: 20px; position: relative; } /* 内容项样式 */ .section .sub-content .content-item { position: relative; height: 73px; cursor: pointer; // transition: all 0.3s ease; border: 1px solid rgba(0, 0, 0, 0.05); border-radius: 6px; overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); &:hover { // transform: translateY(-5px); box-shadow: 0 8px 16px rgba(50, 97, 206, 0.2); border-color: rgba(50, 97, 206, 0.3); .position-img { opacity: 1; } .item { backdrop-filter: blur(5px); // background-color: hsla(0, 0%, 100%, 0.85); } .item-desc { opacity: 0.9; } .item-image img { transform: scale(1.35); } } } /* 内容卡片样式 */ .section .sub-content .content-item .item { display: flex; position: relative; overflow: hidden; border-radius: 6px; width: 100%; height: 100%; z-index: 2; // transition: all 0.3s ease; } /* 图片样式 */ .section .sub-content .content-item .item-image { width: 70px; height: 100%; flex-shrink: 0; overflow: hidden; padding: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f7fa; color: #c9cdd4; svg { font-size: 24px; } } img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.3s ease; } } /* 文本区域样式 */ .section .sub-content .content-item .item-text { flex: 1; padding: 8px 12px; display: flex; flex-direction: column; justify-content: center; overflow: hidden; .item-title { font-size: 14px; color: #171717cc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .item-desc { font-size: 12px; color: #181818; margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; // transition: opacity 0.3s ease; } } /* 背景图片 */ .position-img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; overflow: hidden; // transition: opacity 0.3s ease-in-out; z-index: 1; img { width: 100%; height: 100%; object-fit: cover; filter: blur(2px) brightness(0.9); } } /* 占位空间 */ .spacer { height: 100px; } /* 响应式调整 */ @media (max-width: 992px) { .container { flex-direction: column; padding: 0 16px; } .tabs { position: static; width: 100%; margin-right: 0; max-height: none; display: flex; overflow-x: auto; padding: 10px 0; .tab { flex-shrink: 0; margin: 0 10px; } } .content { margin-left: 0; padding-top: 10px; } } </style> 右侧滚动条滚动的时候左侧tab 被选中

<template> 查询条件 <a-form-model :model="searchForm" layout="inline" ref="searchForm" class="searchForm"> <a-form-model-item label="题库名称" prop="title"> <a-input v-model="searchForm.title" placeholder="请输入题库名称" /> </a-form-model-item> <a-form-model-item class="searchButton"> <a-button type="primary" v-if="QX.read" @click="getSearch">查询</a-button> <a-button type="default" v-if="QX.read" @click="restSearch('searchForm')">重置</a-button> </a-form-model-item> </a-form-model> <a-button type="primary" @click="addBank" icon="plus" v-if="QX.add">新增</a-button> <a-table :columns="columns" :data-source="dataList" :pagination="false" :loading="loading" rowKey="questionBankId" :scroll="{ y: this.$getViewportSize().height - 300 }" > <a-icon class="iconBtn" type="edit" />编辑 <a-divider v-if="QX.edit && QX.delete" type="vertical" /> <a-popconfirm title="确认是否删除?" ok-text="是" cancel-text="否" @confirm="removeBank(record)" > <a-icon class="iconBtn" type="delete" />删除 </a-popconfirm> </a-table> <template slot="action" slot-scope="text, record, index"> 删除 </template> <a-pagination show-size-changer :total="totalPage" :current="pageIndex" :pageSize="pageSize" @showSizeChange="onShowSizeChange" @change="onChangePage" style="float: right; margin-top: 15px" /> <a-drawer :closable="true" :title="title" width="auto" :visible="visible" @close="visible = !visible" > <a-spin class="submitLoading" :spinning="submitLoading"> <a-form-model :model="form" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rules" ref="form" class="lay-drawer-form"> <a-row> <a-col :span="12"> <a-form-model-item label="题库名称" prop="title"> <a-input v-model="form.title" :maxLength="25" placeholder="请输入题库名称" style="width: 380px" /> </a-form-model-item> </a-col> <a-col :span="12"> <a-form-model-item label="关联岗位" prop="positionId"> <a-select v-model="form.positionId" style="width: 380px" placeholder="请选择岗位"> <a-select-option v-for="(label, value) in positionDict" :key="value" :value="value"> {{ label }} </a-select-option> </a-select> </a-form-model-item> </a-col> <a-col :span="12"> <a-form-model-item label="参与PK" prop="participateInPk"> <a-switch v-model="form.participateInPk" :checkedValue="1" :unCheckedValue="0" /> </a-form-model-item> </a-col> </a-row> </a-form-model> <a-button type="primary" @click="downloadTemplate">下载模板</a-button> <a-upload name="file" :showUploadList="false" :beforeUpload="beforeUpload" accept=".xlsx,.xls" :disabled="!importEnabled" > <a-button :disabled="!importEnabled"><a-icon type="upload" /> 导入题目</a-button> </a-upload> <a-button type="dashed" @click="addTopicVisible = true" :disabled="!addEnabled" > <a-icon type="plus" /> 添加题目 </a-button> {{ index + 1 }}. {{ topic.content }} <label v-for="option in getOptions(topic)" :key="option.key"> <input type="radio" :name="'topic' + topic.topicId" :value="option.key" :checked="topic.correctAnswer === option.key" /> {{ option.key }}. {{ option.value }} </label> <label> <input type="radio" :name="'topic' + topic.topicId" value="正确" :checked="topic.correctAnswer === '正确'" /> 正确 </label> <label> <input type="radio" :name="'topic' + topic.topicId" value="错误" :checked="topic.correctAnswer === '错误'" /> 错误 </label> <a-icon type="delete" /> 删除 <a-pagination v-model="topicPageNum" :pageSize="topicPageSize" :total="totalTopicCount" @change="handleTopicPageChange" style="text-align: right;" /> <a-button type="default" @click="visible = !visible" > 取消 </a-button> <a-button type="primary" @click="submitForm" > 确认 </a-button> </a-spin> </a-drawer> <a-drawer title="新增题目" :visible="addTopicVisible" @close="addTopicVisible = false" width="500" > <a-form-model :model="addTopicForm" layout="vertical" :rules="rulesForAddTopic" ref="addTopicFormRef"> <a-form-model-item label="题目类型" prop="topicType"> <a-select v-model="addTopicForm.topicType" style="width: 100%"> <a-select-option :value="1">选择题</a-select-option> <a-select-option :value="2">判断题</a-select-option> </a-select> </a-form-model-item> <a-form-model-item label="题目内容" prop="content"> <a-input v-model="addTopicForm.content" placeholder="请输入题目内容" /> </a-form-model-item> <a-form-model-item label="选项A" prop="optionA"> <a-input v-model="addTopicForm.optionA" placeholder="请输入选项A内容" /> </a-form-model-item> <a-form-model-item label="选项B" prop="optionB"> <a-input v-model="addTopicForm.optionB" placeholder="请输入选项B内容" /> </a-form-model-item> <a-form-model-item label="选项C" prop="optionC"> <a-input v-model="addTopicForm.optionC" placeholder="请输入选项C内容" /> </a-form-model-item> <a-form-model-item label="选项D" prop="optionD"> <a-input v-model="addTopicForm.optionD" placeholder="请输入选项D内容" /> </a-form-model-item> <a-form-model-item label="正确答案" prop="correctAnswer"> <a-select v-model="addTopicForm.correctAnswer" style="width: 100%"> <a-select-option value="A">A</a-select-option> <a-select-option value="B">B</a-select-option> <a-select-option value="C">C</a-select-option> <a-select-option value="D">D</a-select-option> </a-select> </a-form-model-item> <a-form-model-item label="正确答案" prop="correctAnswer"> <a-select v-model="addTopicForm.correctAnswer" style="width: 100%"> <a-select-option value="正确">正确</a-select-option> <a-select-option value="错误">错误</a-select-option> </a-select> </a-form-model-item> <a-button @click="addTopicVisible = false">取消</a-button> <a-button type="primary" @click="saveNewTopic">保存</a-button> </a-form-model> </a-drawer> </template> <script> import { req, fileDownload } from '../../../api/axiosFun'; import preventBack from 'vue-prevent-browser-back'; export default { name: 'Bank', mixins: [preventBack], data() { return { QX: {}, topicQX: {}, topicList: [], totalTopicCount: 0, // 题目总数 topicPageNum: 1, // 当前页码 topicPageSize: 10, // 每页数量 addTopicVisible: false, addTopicForm: { content: '', // 题目内容 topicType: 1, // 题目类型:1=选择题,2=判断题 optionA: '', optionB: '', optionC: '', optionD: '', correctAnswer: '', }, disabled: false, checkedKeys: [], selectAuth: [], treeData: [], positionDict: {}, title: '', labelCol: { span: 4 }, wrapperCol: { span: 20 }, tableHeight: 0, expanded: false, // 筛选条件是否展开 form: { questionBankId: 0, bankCode: '', title: '', positionId: '', participateInPk: true, }, isEdit: false, // 是否是编辑状态 isAdd: false, // 是否是新增状态 importEnabled: false, // 导入题目按钮是否可用 - 默认为不可用 addEnabled: false, // 添加题目按钮是否可用 - 默认为不可用 rules: { positionId: [ { required: true, message: '请选择岗位', trigger: 'blur' }, ], title: [ { required: true, message: '请输入题库名称', trigger: 'blur' }, ], }, rulesForAddTopic: { content: [ { required: true, message: '请输入题目内容', trigger: ['blur', 'change'] }, ], topicType: [ { required: true, message: '请选择题目类型', trigger: 'change' }, ], optionA: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项A', trigger: ['blur', 'change'], }, ], optionB: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项B', trigger: ['blur', 'change'], }, ], optionC: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项C', trigger: ['blur', 'change'], }, ], optionD: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项D', trigger: ['blur', 'change'], }, ], correctAnswer: [ { required: true, message: '请选择正确答案', trigger: 'change' }, ], }, searchForm: { title: '', }, visible: false, dataList: [], columns, loading: false, submitLoading: false, pageIndex: 1, pageSize: 10, totalPage: 0, ops: { vuescroll: {}, scrollPanel: {}, rail: { keepShow: true, }, bar: { hoverStyle: true, onlyShowBarOnScroll: false, // 是否只有滚动的时候才显示滚动条 background: '#F5F5F5', // 滚动条颜色 opacity: 1, // 滚动条透明度 'overflow-x': 'hidden', }, }, }; }, watch: { 'addTopicForm.topicType': function (newVal) { // 当题目类型变化时,触发相关字段的验证 this.$nextTick(() => { if (this.$refs.addTopicFormRef) { // 验证选项字段 if (newVal === 1) { this.$refs.addTopicFormRef.validateFields(['optionA', 'optionB', 'optionC', 'optionD']); } else { // 清除非必填字段的验证状态 this.$refs.addTopicFormRef.clearValidate(['optionA', 'optionB', 'optionC', 'optionD']); } } }); }, visible(newVal, oldVal) { if (!newVal) { this.restForm('form'); this.form.questionBankId = 0; this.checkedKeys = []; this.selectAuth = []; this.treeData = []; // 重置状态标志 this.isEdit = false; this.isAdd = false; } else { // 当抽屉打开时强制更新按钮状态 this.$nextTick(() => { this.$forceUpdate(); }); } }, }, mounted() { this.actionTitle = '操作'; this.getDict('position').then(res => { const dictMap = {}; res.data.forEach(item => { dictMap[item.dicValue] = item.dicDisplayName; }); this.positionDict = dictMap; }); this.getBankList(); }, methods: { // 获取字典数据方法 getDict(type) { return req('get', /dict/getDictItemByNo, { dicNo: type }) .then((res) => { if (res.result === 'success') { return res; } throw new Error(res.message || '获取字典数据失败'); }) .catch((error) => { console.error(获取${type}字典失败:, error); throw error; }); }, /* 新增题库 */ addBank() { this.visible = true; this.disabled = false; this.title = '新增题库'; // 获取功能权限树 req('get', '/bank/getTree', {}).then((res) => { this.treeData = res.data; }); // 初始化题目列表为空 this.topicList = []; this.totalTopicCount = 0; this.topicPageNum = 1; this.isEdit = false; // 不是编辑状态 this.isAdd = true; // 设置为新增状态 // 在新增状态下禁用导入和添加功能 this.importEnabled = false; this.addEnabled = false; // 强制更新按钮状态 this.$nextTick(() => { this.$forceUpdate(); }); }, // 查询 getSearch() { this.pageIndex = 1; this.getBankList(); }, /* 重置查询 */ restSearch(form) { this.restForm(form); this.pageIndex = 1; this.getBankList(); }, /* 删除题库 */ removeBank(record) { this.loading = true; req('post', '/bank/removeBQuestionBank', { questionBankId: record.questionBankId, }).then((res) => { this.loading = false; if (res.result === 'success') { this.$message.success(res.message); this.getBankList(); } }); }, downloadTemplate() { fileDownload('get', '/topic/downloadTemplate', null).then((res) => { const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = '题库模板.xlsx'; link.click(); window.URL.revokeObjectURL(link.href); }).catch(() => { this.$message.error('下载失败'); }); }, beforeUpload(file) { // 显示加载状态 this.loading = true; const formData = new FormData(); formData.append('file', file); formData.append('questionBankId', this.form.questionBankId); // 添加 questionBankId 字段 req('post', '/topic/import', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }) .then((res) => { // 隐藏加载状态 this.loading = false; if (res.result === 'success') { // 确保在DOM更新后再刷新题目列表 this.$nextTick(() => { // 刷新题目列表 this.getTopicList(this.form.questionBankId, this.topicPageNum, this.topicPageSize) .then(() => { this.$message.success('导入成功'); }) .catch(() => { this.$message.error('刷新题目列表失败'); }); }); } else { this.$message.error(res.message || '导入失败'); } }) .catch((error) => { // 隐藏加载状态 this.loading = false; this.$message.error('导入失败,请重试'); console.error('导入题目出错:', error); }); return false; // 阻止默认上传行为 }, // 获取题库下的题目列表(返回Promise以便链式调用) getTopicList(questionBankId, pageNum = 1, pageSize = 10) { return new Promise((resolve, reject) => { req('post', '/topic/list', { questionBankId, page: pageNum, rows: pageSize, }).then((res) => { if (res.result === 'success') { this.topicQX = res.QX; if (!this.topicQX.edit && !this.topicQX.delete) { this.hideAction(); } else { this.topicList = res.data.map((item, index) => ({ index: (pageNum - 1) * pageSize + index + 1, content: item.content, optionA: item.optionA, optionB: item.optionB, optionC: item.optionC, optionD: item.optionD, topicId: item.topicId, correctAnswer: item.correctAnswer, topicType: item.topicType, })); } // 更新总条数用于分页组件 this.totalTopicCount = res.page.totalResult; resolve(res); } else { this.$message.error(res.message || '获取题目列表失败'); reject(res); } }).catch((error) => { this.$message.error('获取题目列表失败'); reject(error); }); }); }, handleTopicPageChange(page) { this.topicPageNum = page; this.getTopicList(this.form.questionBankId, page, this.topicPageSize); }, // 获取选择题的选项 getOptions(topic) { if (topic.topicType === 1) { // 选择题:返回 A/B/C/D 选项 const options = []; if (topic.optionA) options.push({ key: 'A', value: topic.optionA }); if (topic.optionB) options.push({ key: 'B', value: topic.optionB }); if (topic.optionC) options.push({ key: 'C', value: topic.optionC }); if (topic.optionD) options.push({ key: 'D', value: topic.optionD }); return options; } else if (topic.topicType === 2) { // 判断题:直接返回 正确/错误 return [ { key: '正确', value: '正确' }, { key: '错误', value: '错误' }, ]; } return []; }, checkAnswer(topic, selectedAnswer) { // 仅记录用户选择的答案,不进行是否正确的判断 this.$set(topic, 'userAnswer', selectedAnswer); }, resetAddTopicForm() { this.addTopicForm = { content: '', topicType: 1, optionA: '', optionB: '', optionC: '', optionD: '', correctAnswer: '', }; }, saveNewTopic() { this.$refs.addTopicFormRef.validate((valid, fields) => { if (!valid) { console.log('表单验证失败:', fields); // 找出第一个错误字段并聚焦 const firstErrorField = Object.keys(fields).find(key => fields[key]); if (firstErrorField && this.$refs.addTopicFormRef) { const formItem = this.$refs.addTopicFormRef.$children.find( child => child.prop === firstErrorField, ); if (formItem && formItem.$el) { const input = formItem.$el.querySelector('input, select, textarea'); if (input) input.focus(); } } return; } // 验证通过,处理保存逻辑 const newTopic = { ...this.addTopicForm }; // 发送请求保存题目 req('post', '/topic/add', { questionBankId: this.form.questionBankId, optionA: newTopic.optionA, optionB: newTopic.optionB, optionC: newTopic.optionC, optionD: newTopic.optionD, correctAnswer: newTopic.correctAnswer, content: newTopic.content, topicType: newTopic.topicType, }).then((res) => { if (res.result === 'success') { this.$message.success('题目添加成功'); this.addTopicVisible = false; this.getTopicList(this.form.questionBankId); // 刷新题目列表 } else { this.$message.error(res.message || '保存失败'); } this.resetAddTopicForm(); }).catch((err) => { this.$message.error('网络异常,请重试'); }); }); }, /* 删除题库下的题目 */ removeQuestion(topicId) { this.$confirm({ title: '确认删除该题目?', content: '删除后将无法恢复', okText: '是', cancelText: '否', onOk: () => { req('post', '/topic/removeBTopic', { topicId, questionBankId: this.form.questionBankId, }).then((res) => { if (res.result === 'success') { this.$message.success(res.message); // 刷新题目列表 this.getTopicList(this.form.questionBankId, this.topicPageNum, this.topicPageSize); } else { this.$message.error(res.message || '删除失败'); } }); }, }); }, editAuth(record) { this.loading = true; req('post', '/bank/getBQuestionBank', { questionBankId: record.questionBankId, }).then((res) => { this.loading = false; if (res.result === 'success') { this.visible = true; this.disabled = true; this.title = '修改题库'; this.isEdit = true; // 设置为编辑状态 this.isAdd = false; // 不是新增状态 // 在编辑状态下启用导入和添加功能 this.importEnabled = true; this.addEnabled = true; // 强制更新按钮状态 this.$nextTick(() => { this.$forceUpdate(); }); const bank = res.data; this.$nextTick(() => { this.form.questionBankId = bank.questionBankId; this.form.title = bank.title; this.form.participateInPk = Boolean(bank.participateInPk); this.form.positionId = bank.positionId; this.treeData = bank.treeData; this.checkedKeys = bank.auths; // 获取题目列表 this.topicPageNum = 1; this.getTopicList(bank.questionBankId, this.topicPageNum, this.topicPageSize); }); } }); }, /* 保存or修改题库信息 */ submitForm() { this.$refs.form.validate((valid) => { if (valid) { this.form.participateInPk = this.form.participateInPk ? 1 : 0; const url = this.form.questionBankId ? 'edit' : 'add'; const selectAuth = this.selectAuth; this.form.selectAuth = JSON.stringify(selectAuth); this.submitLoading = true; req('post', /bank/${url}, this.form).then((res) => { if (res.result === 'success') { this.visible = false; this.getBankList(); // 如果是新增题库且成功,获取题目列表 if (!this.form.questionBankId && res.data && res.data.questionBankId) { this.form.questionBankId = res.data.questionBankId; this.topicPageNum = 1; this.getTopicList(res.data.questionBankId, this.topicPageNum, this.topicPageSize); // 新增成功后启用导入和添加功能 this.importEnabled = true; this.addEnabled = true; } this.$message.success(res.message); // 重置新增/编辑状态 this.isEdit = false; this.isAdd = false; } this.submitLoading = false; }); } }); }, /* 重置表单 */ restForm(form) { this.$refs[form].resetFields(); }, /* 改变页数事件 */ onChangePage(page, pageSize) { this.pageIndex = page; this.getBankList(); }, /* 改变每页显示条数 */ onShowSizeChange(current, pageSize) { this.pageIndex = 1; this.pageSize = pageSize; this.getBankList(); }, /* 题库信息列表 */ getBankList() { this.loading = true; this.searchForm.page = this.pageIndex; this.searchForm.rows = this.pageSize; req('post', '/bank/list', this.searchForm) .then((res) => { if (res.result === 'success') { this.dataList = res.data; this.QX = res.QX; // 无权限隐藏操作列 if (!this.QX.edit && !this.QX.delete) { this.hideAction(); } else if (columns[columns.length - 1].title != '操作') { columns.push(actionShow); } this.totalPage = res.page.totalResult; } this.loading = false; }).catch((error) => { this.loading = false; }); }, /* 无所有行操作权限时,隐藏操作栏 */ hideAction() { if (columns[columns.length - 1].title == '操作') { columns.splice(columns.length - 1, 1); } }, /* 校验代号类型 */ validCode(value) { if (value.length > 20) { value = value.slice(0, 20); } for (let i = value.length - 1; i >= 0; i--) { const unicode = value.charCodeAt(i); if (unicode > 65280 && unicode < 65375) { value = value.substr(0, i); } } this.value = value; }, }, }; const actionShow = { title: '操作', width: '200px', hide: true, dataIndex: 'action', key: 'action', align: 'center', scopedSlots: { customRender: 'action' }, }; const columns = [ { title: '序号', width: '50px', align: 'center', customRender: (text, row, index) => index + 1, }, { title: '题库名称', align: 'center', dataIndex: 'title', key: 'title', width: '120px', }, { title: '涉及岗位', align: 'center', dataIndex: 'positionName', key: 'positionName', width: '110px', }, { title: '题目数量', dataIndex: 'topicCount', key: 'topicCount', align: 'center', width: '180px', }, { title: '操作', width: '150px', hide: true, dataIndex: 'action', key: 'action', align: 'center', scopedSlots: { customRender: 'action' }, }, ]; </script> <style scoped> .topic-list-container { max-height: calc(100vh - 400px); /* 根据实际布局调整最大高度 */ overflow-y: auto; } .pagination-wrapper { position: sticky; bottom: 0; left: 0; right: 0; background: #fff; padding: 10px; z-index: 1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-top: 15px; } .topic-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; } .topic-item { border: 1px solid #e8e8e8; padding: 15px; border-radius: 4px; position: relative; /* 为绝对定位的子元素提供定位上下文 */ } .topic-content { font-size: 16px; margin-bottom: 10px; } .options label { display: block; margin: 5px 0; font-size: 14px; } .options input[type="radio"] { margin-right: 5px; } /* 按钮禁用状态样式优化 */ .ant-upload.ant-upload-disabled .ant-upload-list, .ant-btn[disabled] { cursor: not-allowed; opacity: 0.5; } .topic-delete { position: absolute; top: 10px; right: 10px; } </style> <style scoped> @import "../../../assets/css/maincss.css"; </style> 在编辑页面中选择某个题目的选项后,再次打开该题时,默认显示的是上次选择的选项而不是正确答案。修改此问题

<template> <el-row :gutter="20"> <el-col :span="24"> <el-form :inline="true" label-width="100px" @submit.prevent="getList"> <el-form-item label="名称"> <el-input v-model="queryParams.name" placeholder="请输入名称" clearable /> </el-form-item> <el-form-item label="责任人"> <el-input v-model="queryParams.respPerson" placeholder="请输入责任人" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="getList">查询</el-button> <el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="toggleGantt" style="margin-left: 10px;" > {{ showGantt ? '收起甘特图' : '展开甘特图' }} </el-button> </el-form-item> </el-form> </el-col> <el-col :span="showGantt ? 12 : 24"> <el-table ref="table" :data="listData" row-key="uid" border :row-style="{ height: '30px' }" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" @row-click="handleRowClick" @expand-change="handleExpandChange" highlight-current-row > <el-table-column prop="code" label="编号" width="120" /> <el-table-column prop="name" label="名称" min-width="180" /> <el-table-column prop="respPerson" label="责任人" width="120" /> <el-table-column prop="schedule" label="完成百分比" width="120"> <template slot-scope="{row}"> <el-progress :percentage="row.schedule" :show-text="row.schedule > 10" :stroke-width="18" :color="getProgressColor(row.schedule)" /> </template> </el-table-column> <el-table-column prop="planStartDate" label="计划开始日期" width="150" /> <el-table-column prop="planEndDate" label="计划结束日期" width="150" /> <el-table-column label="操作" width="100"> <template slot-scope="scope"> <el-button size="mini" icon="el-icon-view" @click.stop="handleUpdate(scope.row)">查看</el-button> </template> </el-table-column> </el-table> </el-col> <el-col v-if="showGantt" :span="12"> </el-col> </el-row> <el-dialog :title="title" :visible.sync="open" width="850px" append-to-body> <el-form ref="form" :model="form" :rules="rules" label-width="100px" :disabled="disable"> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="code"> <el-input v-model="form.code" placeholder="请输入编号" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="name"> <el-input v-model="form.name" placeholder="请输入名称" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" type="textarea" placeholder="请输入备注" rows="3" /> </el-form-item> </el-col> </el-row> <el-button @click="cancel">取 消</el-button> </el-form> </el-dialog> </template> <script> import gantt from 'dhtmlx-gantt'; import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'; import { getPlan, listPlan } from '@/api/dw/plan/planview'; export default { name: 'Planview', data() { return { expandedKeys: new Set(), // 存储所有展开节点的UID listData: [], total: 0, queryParams: { pageNum: 1, pageSize: 1000, // 树形结构不适合分页,增加单页大小 name: null, respPerson: null }, open: false, title: '', form: {}, rules: { name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], schedule: [ { required: true, message: '完成百分比不能为空', trigger: 'blur' }, { type: 'number', message: '输入内容不是有效的数字', trigger: 'blur' } ] }, disable: true, showGantt: true, // 控制甘特图显示 flatData: [], // 扁平化数据 ganttInitialized: false, // 甘特图初始化标志 currentSelectedTask: null, // 当前选中的任务ID ganttExpandState: new Map() // 存储甘特图的展开状态 }; }, mounted() { this.getList(); }, methods: { // 获取进度条颜色 getProgressColor(percentage) { if (percentage < 30) return '#F56C6C'; if (percentage < 70) return '#E6A23C'; return '#67C23A'; }, // 初始化甘特图 initGantt() { if (!this.$refs.ganttContainer) return; try { // 清除之前的实例 if (gantt.$container) { gantt.destructor(); } gantt.config.date_format = '%Y-%m-%d'; gantt.config.scale_unit = 'month'; gantt.config.step = 1; gantt.config.subscales = [ { unit: 'day', step: 1, date: '%j, %D' } ]; // 关键修改:去掉 columns 配置 // gantt.config.columns = [] // 完全去掉列配置 // 调整行高与表格一致 gantt.config.row_height = 30; gantt.config.scale_height = 30; // 隐藏网格部分 gantt.config.show_grid = false; // 调整时间线区域宽度为100% gantt.config.grid_width = 0; gantt.templates.task_text = (start, end, task) => task.text; gantt.init(this.$refs.ganttContainer); // 绑定事件 gantt.attachEvent('onTaskSelected', (id) => { this.currentSelectedTask = id; this.scrollToTableRow(id); }); // 绑定展开/折叠事件 gantt.attachEvent('onAfterTaskOpen', (id) => { this.ganttExpandState.set(id, true); this.syncGanttExpandToTable(id, true); }); gantt.attachEvent('onAfterTaskClose', (id) => { this.ganttExpandState.set(id, false); this.syncGanttExpandToTable(id, false); }); // 获取甘特图DOM元素 const ganttDataArea = document.querySelector('.gantt_data_area'); // 监听表格滚动事件 const tableBody = this.$refs.table.$el.querySelector('.el-table__body-wrapper'); if (tableBody) { tableBody.addEventListener('scroll', () => { // 同步垂直滚动 if (ganttDataArea) { ganttDataArea.scrollTop = tableBody.scrollTop; } }); } this.ganttInitialized = true; console.log('甘特图初始化成功'); } catch (e) { console.error('甘特图初始化失败:', e); } }, // 将甘特图的展开状态同步到表格 syncGanttExpandToTable(taskId, expanded) { const row = this.flatData.find(item => item.uid === taskId); if (!row) return; // 更新展开状态 if (expanded) { this.expandedKeys.add(row.uid); } else { this.expandedKeys.delete(row.uid); } // 更新表格UI this.$nextTick(() => { const tableRow = this.$refs.table.$el.querySelector([data-id="${row.uid}"]); if (tableRow) { const expandIcon = tableRow.querySelector('.el-table__expand-icon'); if (expandIcon) { const isExpanded = expandIcon.classList.contains('el-table__expand-icon--expanded'); if (isExpanded !== expanded) { this.$refs.table.toggleRowExpansion(row, expanded); } } } }); }, // 获取数据 async getList() { try { const res = await listPlan(this.queryParams); this.listData = this.handleTree(res.data, 'uid', 'parentUid'); this.flatData = this.flattenTree(this.listData); // 初始展开所有节点 this.expandedKeys = new Set(this.flatData.map(item => item.uid)); this.$nextTick(() => { // 初始化甘特图 if (this.showGantt) { this.initGantt(); this.updateGantt(); } // 展开所有节点 this.expandAllNodes(); }); } catch (error) { console.error('获取数据失败:', error); } }, // 递归展开所有节点 expandAllNodes() { if (!this.$refs.table || !this.listData.length) return; const expandNode = (node) => { this.$refs.table.toggleRowExpansion(node, true); if (node.children && node.children.length) { node.children.forEach(child => expandNode(child)); } }; this.listData.forEach(root => expandNode(root)); }, // 更新甘特图数据 updateGantt() { if (!this.ganttInitialized) return; const tasks = this.getVisibleTasks(); console.log('更新甘特图任务数量:', tasks.length); try { // 保存当前甘特图的展开状态 this.saveGanttExpandState(); gantt.clearAll(); gantt.parse({ data: tasks, links: [] }); // 恢复甘特图的展开状态 this.restoreGanttExpandState(); this.adjustGanttView(tasks); } catch (e) { console.error('更新甘特图失败:', e); } }, // 保存甘特图的展开状态 saveGanttExpandState() { if (!this.flatData.length) return; // 遍历所有任务,保存展开状态 this.flatData.forEach(item => { if (gantt.isTaskExists(item.uid)) { this.ganttExpandState.set(item.uid, gantt.isTaskOpen(item.uid)); } }); }, // 恢复甘特图的展开状态 restoreGanttExpandState() { this.ganttExpandState.forEach((isOpen, taskId) => { if (gantt.isTaskExists(taskId)) { gantt.openTask(taskId, isOpen); } }); }, // 获取当前可见的任务(根据展开状态) getVisibleTasks() { const visibleTasks = []; const collectVisible = (nodes) => { nodes.forEach(node => { visibleTasks.push({ id: node.uid, text: node.name, start_date: node.planStartDate, duration: node.planDuration || 1, progress: (node.schedule || 0) / 100, parent: node.parentUid || 0, open: this.expandedKeys.has(node.uid) // 设置初始展开状态 }); // 如果节点是展开的,递归收集子节点 if (this.expandedKeys.has(node.uid) && node.children) { collectVisible(node.children); } }); }; collectVisible(this.listData); return visibleTasks; }, // 自动调整甘特图视图 adjustGanttView(tasks) { if (!tasks.length) return; // 计算时间范围 const dates = tasks .filter(t => t.start_date) .map(t => new Date(t.start_date)); if (!dates.length) return; const minDate = new Date(Math.min(...dates.map(d => d.getTime()))); const maxDate = new Date(Math.max(...dates.map(t => { const endDate = new Date(t.start_date); endDate.setDate(endDate.getDate() + (t.duration || 0)); return endDate.getTime(); }))); // 设置时间范围 gantt.setWorkTime({ start_date: minDate, end_date: maxDate }); // 根据时间跨度调整缩放级别 const timeDiffInDays = Math.ceil((maxDate - minDate) / (1000 * 60 * 60 * 24)); if (timeDiffInDays <= 7) { gantt.config.scale_unit = 'day'; gantt.config.step = 1; } else if (timeDiffInDays <= 31) { gantt.config.scale_unit = 'week'; gantt.config.step = 1; } else if (timeDiffInDays <= 365) { gantt.config.scale_unit = 'month'; gantt.config.step = 1; } else { gantt.config.scale_unit = 'year'; gantt.config.step = 1; } gantt.render(); }, // 树形结构转扁平结构 flattenTree(data) { const result = []; const stack = [...data]; while (stack.length) { const node = stack.pop(); result.push(node); if (node.children) { stack.push(...node.children); } } return result; }, // 处理树形结构 handleTree(data, idKey = 'uid', parentKey = 'parentUid') { const map = {}; const tree = []; // 创建映射 data.forEach(item => { map[item[idKey]] = { ...item, children: [] }; }); // 构建树 data.forEach(item => { const parentId = item[parentKey]; if (parentId && map[parentId]) { map[parentId].children.push(map[item[idKey]]); } else { tree.push(map[item[idKey]]); } }); return tree; }, // 行点击事件 handleRowClick(row) { this.$nextTick(() => { // 高亮当前行 this.$refs.table.setCurrentRow(row); // 在甘特图中选中对应任务 if (this.ganttInitialized) { gantt.selectTask(row.uid); gantt.showTask(row.uid); } }); }, // 滚动到表格行 scrollToTableRow(taskId) { const row = this.flatData.find(item => item.uid === taskId); if (!row) return; this.$nextTick(() => { // 确保所有父节点都展开 this.expandParents(row); // 高亮当前行 this.$refs.table.setCurrentRow(row); // 滚动到元素 const tableBody = this.$refs.table.$el.querySelector('.el-table__body-wrapper'); const rowEl = this.$refs.table.$el.querySelector([data-id="${row.uid}"]); if (tableBody && rowEl) { const rowTop = rowEl.offsetTop; const tableHeight = tableBody.clientHeight; tableBody.scrollTop = rowTop - tableHeight / 2; } }); }, // 展开父节点 expandParents(row) { if (!row.parentUid) return; const parent = this.flatData.find(item => item.uid === row.parentUid); if (parent && !this.expandedKeys.has(parent.uid)) { this.expandedKeys.add(parent.uid); this.$refs.table.toggleRowExpansion(parent, true); this.expandParents(parent); } }, // 树展开/折叠更新甘特图 handleExpandChange(row, expanded) { // 更新展开状态 if (expanded) { this.expandedKeys.add(row.uid); } else { this.expandedKeys.delete(row.uid); // 折叠时同时折叠所有子节点 this.collapseChildren(row); } // 更新甘特图 this.$nextTick(() => { this.updateGantt(); // 同步到甘特图展开状态 if (this.ganttInitialized && gantt.isTaskExists(row.uid)) { gantt.openTask(row.uid, expanded); } }); }, // 递归折叠子节点 collapseChildren(node) { if (node.children && node.children.length) { node.children.forEach(child => { this.expandedKeys.delete(child.uid); this.$refs.table.toggleRowExpansion(child, false); this.collapseChildren(child); }); } }, // 切换甘特图显示 - 解决重新初始化问题 toggleGantt() { this.showGantt = !this.showGantt; if (this.showGantt) { this.$nextTick(() => { // 确保每次展开都重新初始化甘特图 this.ganttInitialized = false; this.initGantt(); this.updateGantt(); }); } }, // 获取数据详情 async handleUpdate(row) { try { const res = await getPlan(row.uid); this.form = res.data; this.open = true; this.title = '查看治理计划'; } catch (error) { console.error('获取详情失败:', error); } }, // 取消按钮 cancel() { this.open = false; }, // 重置查询 resetQuery() { this.queryParams = { pageNum: 1, pageSize: 1000, name: null, respPerson: null }; this.getList(); } } }; </script> <style scoped> /* 新增样式 */ .page-container { padding: 20px; background-color: #f5f7fa; height: calc(100vh - 60px); } .search-wrapper { background-color: #fff; padding: 15px 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); margin-bottom: 20px; } .table-container { background-color: #fff; padding: 15px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); height: calc(100vh - 180px); overflow: auto; } .gantt-container { background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 5px; height: calc(100vh - 180px); overflow: auto; } /* 表格标题行样式 */ :deep(.el-table__header) th { height: 30px; padding: 0; background-color: #f8f8f9; } /* 甘特图标题行样式 */ :deep(.gantt_grid_head_cell) { height: 30px; line-height: 30px; padding: 0 5px; background-color: #f8f8f9; border-right: 1px solid #e0e0e0; font-weight: bold; } /* 甘特图时间刻度样式 */ :deep(.gantt_scale_cell) { height: 30px; line-height: 30px; border-bottom: 1px solid #e0e0e0; background-color: #f8f8f9; } /* 对齐表格和甘特图的边框 */ :deep(.el-table--border) { border: 1px solid #ebeef5; } :deep(.gantt_grid, .gantt_grid_head, .gantt_data_area) { border: 1px solid #ebeef5; } .page-container { padding: 20px; background-color: #f5f7fa; } .search-wrapper { background-color: #fff; padding: 15px 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); margin-bottom: 20px; } .table-container { background-color: #fff; padding: 15px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .gantt-container { background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 5px; } .dialog-footer { text-align: right; margin-top: 20px; } .toggle-button { margin-bottom: 15px; } .el-table { width: 100%; } .el-table--border { border: 1px solid #ebeef5; } .el-table__row:hover { background-color: #f5f7fa !important; } .el-progress { margin-top: 8px; } .el-form-item { margin-bottom: 18px; } </style> 甘特图部分,时间维度改为年月,下面为日期

<template> <logo></logo> <animate-b-g-image name="passengerflowOd-wrap"></animate-b-g-image> <select-model :typeName="'客流OD'"></select-model> <area-module class="left-wrap" :nowIndex="nowIndex" @getAreaInfo="customAreaData" ref="searchChild" @transferData="transferData" :editingStatus="editingStatus"> </area-module> <right-module class="right-wrap" :nowIndex="nowIndex" :isShowAllArea="showAllArea" :searchParams="searchParams" :rightMode="rightMode" :liquidFillChartInfo="liquidFillChartInfo" :odTableList="odTableList" @changeFlowEvent="changeFlowEvent" :flag="flag" @getLineMap="getLineMap" @enlargeBtn="enlargeBtn"> </right-module> OD线 <el-slider v-model="sliderOdLine" range :marks="marks" :max="legendList.oDNumMax" @change="changeOdLine" :disabled="sliderDisabled"> </el-slider> {{ item.label }} {{legendList.oDNumMax}} {{legendList.oDNumMid}} {{legendList.oDNumMin}} <el-select v-model="searchKeys" placeholder="请输入站点名" suffix-icon="el-icon-search" class="search-input" filterable remote clearable :remote-method="remoteMethod" @change="checkValue" @clear="clearMarker"> <el-option v-for="(item, index) in searchSiteValues" :key="index" :label="item.stationName + '('+ item.roadName +')'" :value="index"> </el-option> </el-select> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" :inline="true"> <el-form-item prop="name"> <el-input placeholder="设定区域名称" clearable v-model="ruleForm.name"> </el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">保存</el-button> </el-form-item> </el-form> <InfoWindow v-show="showInfoWindow" ref="infoWindow" :itemObj="infoWindowData" :info-window="stationInfoWindow" :infoTitle="infoTitle" :expandStatus="false"></InfoWindow> <el-dialog :visible.sync="dialogVisible" :before-close="handleClose" id="DvelopmentTargets" custom-class="my-dialog"> </el-dialog> </template> <script> import API_PASSENGEROD from '@/api/api_passengerOD' import VueSlider from 'vue-slider-component' import 'vue-slider-component/theme/default.css' import selectModel from "@/components/selectModel"; import "./../layout/layout.scss"; import InfoWindow from "@/components/stationInfoWindows.vue" import areaModule from "./areaModule"; import rightModule from "./rightModule"; import { AMapManager, lazyAMapApiLoaderInstance } from "vue-amap"; import tableList from "@/components/tableList"; let lineColor = null; // od线颜色 // 站点地图 let stationPathSimplifierIns = null; let showStations = null; let hideStations = null; // 线路地图 let linePathSimplifierIns = null; let showLine = null; let hideLine= null; // 选中站点od let lineStationPathSimplifierIns = null; let showStationLine = null; let hideStationLine = null; // 区域地图 let areaPathSimplifierIns = null; let showAreaLine = null; let hideAreaLine = null; // 展示圆多边形 let circleCase = null; let polygonCase = null; let siteMarker = null; // 搜索站点 // 绘制编辑圆多边形 let circleEditor = null; let polyEditor = null; import AnimateBGImage from "@/components/AnimateBGImage"; import logo from '@/components/logo' let layer1 = null let layer2 = null let layer3 = null let markIcon1 = null let markIcon2 = null let markIcon3 = null export default { name: "passengerFlowOd", components: { selectModel, VueSlider, areaModule, rightModule, InfoWindow, tableList, AnimateBGImage, logo }, data() { return { max : null, mid : null, min : null, layerList : [], redIcon:require('../../assets/red.gif'), yellowIcon:require('../../assets/yellow.gif'), tableDataEnlargeBtn:[], dialogVisible:false, selectLineCase:{ data:[], id:'' }, showInfoWindow: false, stationInfoWindow: null, // 信息框 infoTitle:'', radius: '', center: [], path: [], pathArea: '', mouseTool: null, ruleForm: { name: '', }, rules: { name: [{ required: true, message: '请输入区域名称', trigger: 'blur' }, { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' } ], }, searchSiteValues: [], // 模糊搜索数据 searchKeys: '', // 搜索关键词 siteCenter: [], // 搜索站点坐标 infoWindowData: [{ overviewTitle: '区域面积', overviewValue: '', }, { overviewTitle: '流入数量', overviewValue: '', }, { overviewTitle: '流出数量', overviewValue: '', } ], searchParams: {}, //查询条件 searchId: '', odTableList: [], //top10表格数据 titleSrc: require("../../assets/images/common/bcImg.png"), sliderOdLine: [null, null], odLineData: [], legendList: { oDNumMin: null, oDNumMid: null, oDNumMax: null, // OD最大 }, marks: {}, options: [{ value: 0, label: "站点", icon: require("../../assets/images/layout/lines.png"), }, { value: 1, label: "线路", icon: require("../../assets/images/layout/site.png"), }, { value: 2, label: "区域", icon: require("../../assets/images/layout/vehicle.png"), }, ], showGraphicsMode: true, showCustomMode: true, savaState: false, nowIndex: 0, //0 站点 1线路 2区域 checkModeIndex: '', // 自定义圆坐标 customCircleData: { radius: '', location: [], }, customPolygonData: { areaCoverage: '', // 面积 location: [], }, // 自定义矩形坐标 liquidFillChartInfo: { chartId: "coverAreaRateInfo", chartName: "流入/流出占比", chartValue: [0.4, 0.6], }, showAllArea: true, rightMode: true, dotOptions: [{ tooltip: 'always' }, { tooltip: 'always' }], isLoading: false, // od线加载 exportParams: {}, odLinePrams: { ODNumBefore: '', ODNumAfter: '', }, flag: false, // 流入流出渲染 editingStatus: true, allAreaState: false, // 全城od线 customState: false, // 自定义od线 stationMapData:[], lineMapData:[], checkLineObj:{ linePath:[], lineStationInfo:[], lineName:'', passengerNum:'', stationNum:"", timeImbalanceFactor:"", }, checkStationOd:[], map:null, sliderDisabled:false, stationMarkerArr:[], } }, methods: { /** * @Description: 清空地图信息 飞线图 站点 * @author konghaitao * @date 2023-2-16 */ clearMap(){ if(layer1 ){ layer1.setMap(null); layer2.setMap(null); layer3.setMap(null); } if(markIcon1){ markIcon1.setMap(null); markIcon2.setMap(null); } }, //放大功能 enlargeBtn(data) { console.log(data,'dataaa') $(".my-dialog").css({ margin: "0 auto ", width: "80%", 'max-height':data.tableList.length <= 10 ? '50%':"90%", padding: "1%", top: data.tableList.length <= 10 ? '20%' :'5%' }); this.tableDataEnlargeBtn = data this.dialogVisible = true; }, //关闭 handleClose(done) { $(".el-dialog__wrapper").css({ "z-index": "-11100", }); this.$nextTick(() => { done(); }); }, remoteMethod(query) { let that = this; if (query !== "") { setTimeout(() => { that.getSearchStation(query); }, 500); } }, getSearchStation(query) { API_PASSENGEROD .getStationInfo({ stationName: query, }) .then((response) => { if (response.code == 0) { this.searchSiteValues = response.value; } else { console.log(response.message); } }) .catch((error) => { console.log(error) }); }, // 站点搜索 checkValue(val) { var that = this; if (this.searchKeys !== "") { this.siteCenter = [that.searchSiteValues[val].longitude, that.searchSiteValues[val].latitude] this.$nextTick(() => { that.drawSiteMap() }) } }, drawSiteMap() { var that = this; siteMarker && this.map.remove(siteMarker) siteMarker = new AMap.Marker({ icon: "//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png", position: that.siteCenter, offset: new AMap.Pixel(-13, -30) }); siteMarker.setMap(that.map); this.map.setFitView([siteMarker]) }, // 清除覆盖 clearMarker() { siteMarker && this.map.remove(siteMarker) }, // 更改模块 changeModule(value) { this.odTableList = [] this.nowIndex = value }, getDay(day) { var today = new Date(); var targetday_milliseconds = today.getTime() + 1000 * 60 * 60 * 24 * day; today.setTime(targetday_milliseconds); //注意,这行是关键代码 var tYear = today.getFullYear(); var tMonth = today.getMonth(); var tDate = today.getDate(); tMonth = this.doHandleMonth(tMonth + 1); tDate = this.doHandleMonth(tDate); return tYear + tMonth + tDate; }, doHandleMonth(month) { var m = month; if (month.toString().length == 1) { m = "0" + month; } return m; }, /**滑动od线 */ changeOdLine(val) { this.sliderOdLine = val; switch(this.nowIndex) { case 0: if(this.searchParams.platformIdOff == '' && this.searchParams.platformIdOn == '') { this.getStationMap(this.searchParams,false,0); } else { this.getStationMap(this.searchParams,false,1); } // false 滑动od线时不重置起始 break case 1: if(this.searchParams.lineId == '') { this.defaultLineOD(this.searchParams,false) } else { this.getLineStationPassengersOD(this.searchParams,false,1) } break case 2: this.getStationPathOfAreaInfo( this.searchParams, false) break } }, transferData(data) { this.map && this.map.clearMap(); this.searchParams = data; this.customState = false; this.allAreaState = false; this.sliderOdLine = [null, null]; console.log('bbbbbbb') console.log('cccc',new Loca.LinkLayer) this.getInfo(data) }, //获取当前模块所有信息 getInfo(data){ switch (this.nowIndex) { case 0: this.sliderDisabled = false; this.sliderOdLine = [null, null]; // 隐藏 线路 区域地图数据 linePathSimplifierIns && hideLine(); lineStationPathSimplifierIns && hideStationLine(); this.map && this.map.clearMap(); if(this.stationMarkerArr.length > 0) { this.stationMarkerArr.map((item,index)=>{ this.map.remove(item); }) } lineStationPathSimplifierIns && hideStationLine(); areaPathSimplifierIns && hideAreaLine(); // 清除线路 if(data.platformIdOff == '' && data.platformIdOn == ''){ this.showAllArea = true; this.getRoadOD(data); this.getStationMap(data,true,0); // //未选择站点图 }else { this.showAllArea = false; //查询表格信息及站点坐标 this.getStationOD() this.getStationMap(data,true,1); // 1为选中站图 } break; case 1: //线路 // 隐藏站点地图 this.sliderOdLine = [null, null]; stationPathSimplifierIns && hideStations(); linePathSimplifierIns && hideLine(); lineStationPathSimplifierIns && hideStationLine(); this.map && this.map.clearMap(); if(this.stationMarkerArr.length > 0) { this.stationMarkerArr.map((item,index)=>{ this.map.remove(item); }) }; lineStationPathSimplifierIns && hideStationLine(); areaPathSimplifierIns && hideAreaLine(); if(data.lineId == '') { this.showAllArea = true; this.sliderDisabled = false; this.getLineOD() //线路top10 this.defaultLineOD(data,true); // 默认线路 }else { //查询单个线路信息 this.showAllArea = false; this.sliderDisabled = true; this.getLineSingelTable(); this.checkLineOD(data); } break; case 2: // 隐藏站点线路地图 this.sliderDisabled = false; this.sliderOdLine = [null, null]; linePathSimplifierIns && hideLine(); stationPathSimplifierIns && hideStations(); linePathSimplifierIns && hideLine(); this.map && this.map.clearMap(); if(this.stationMarkerArr.length > 0) { this.stationMarkerArr.map((item,index)=>{ this.map.remove(item); }) }; // 清除站点 // 清除站点od线 lineStationPathSimplifierIns && hideStationLine(); this.customAreaData(data.id) // 区域图+ 流入流出占比 this.getStationOfAreaInfo(data.id, true) // 自定义区域表格 console.log(data,'33333') this.getStationPathOfAreaInfo(data, true) // 自定义区域Od线 // } break; } }, //查询单个线路信息 getLineSingelTable(){ let params = this.searchParams if(!params.lineId){ return } params.top = 10 API_PASSENGEROD.getRoadSectionPassenger(params).then(response => { if(response.value.length){ this.showAllArea = false this.odTableList = response.value console.log( this.odTableList ,'查询单个线路信息') }else { this.odTableList = [] } }).catch(err =>{ this.$message.error(err); }) }, //查询线路top10 getLineOD(){ let params = this.searchParams params.byLine = true params.top = 10 API_PASSENGEROD.getRegionalTravelOD(params).then(response=>{ if(response.value.length){ this.showAllArea = true this.odTableList = response.value } else { this.odTableList = [] } }).catch(err =>{ this.$message.error(err); }) }, //获取站点表格数据及坐标 getStationOD(flag){ // if(flag){ // this.searchParams.platformIdOff = this.searchParams.stationPointId // this.searchParams.platformIdOn = null // } else { // this.searchParams.platformIdOn = this.searchParams.stationPointId // this.searchParams.platformIdOff = null // } API_PASSENGEROD.getStationOD( this.searchParams).then(response => { console.log(response,'表格数据') if(response.value.length){ this.showAllArea = false this.odTableList = response.value } else { this.odTableList = [] } }).catch(err =>{ this.$message.error(err); }) }, getLineMap(data) { var that = this; if( data.id === that.selectLineCase.id){ that.map.remove( that.selectLineCase.data); that.selectLineCase = { data :[], id :'', } } else { that.demo(data) } }, demo(data){ var that = this; that.map.remove(that.selectLineCase.data); that.selectLineCase = { data :[], id :'', } let newRouteLine = new AMap.Polyline({ path: data.stationLocation, strokeColor: "#a9db35", isOutline: false, geodesic: true, // 大地线 strokeOpacity: 1, strokeWeight: 4, strokeStyle: "solid", strokeDasharray: [10, 5], lineJoin: "round", lineCap: "round", borderWeight: 2, outlineColor: "transparent", }); that.selectLineCase = { data:newRouteLine, id:data.id } newRouteLine.setMap(that.map); }, /* initMapData(data) { var that = this; this.odLineData = []; this.isLoading = true; let params = data; params.ODNumBefore = this.sliderOdLine[0]; params.ODNumAfter = this.sliderOdLine[1]; API_PASSENGEROD .getODNum3(params) .then((response) => { if (response.code === 0) { if (this.allAreaState) { let data = response.value.stationPathDto.splice(0, 100); if (data.length > 0) { data.map((item, index) => { that.odLineData.push({ oDNum: item.oDNum, path: [ [item.startLongitude, item.startLatitude], [item.endLongitude, item.endLatitude] ], startStationName: item.startStationName, endStationName: item.endStationName, }) }) this.isLoading = false; } else { this.isLoading = false; } } else { this.legendList.oDNumMin = response.value.oDNumMin; this.legendList.oDNumMid = response.value.oDNumMid; this.legendList.oDNumMax = response.value.oDNumMax; let keyData = [0, this.legendList.oDNumMin, this.legendList.oDNumMid, this.legendList.oDNumMax]; let valData = [String(0), String(this.legendList.oDNumMin), String(this.legendList.oDNumMid), String( this.legendList.oDNumMax)] let obj = {}; for (let i = 0; i < valData.length; i++) { const key = keyData[i]; obj[key] = valData[i] } this.marks = obj; this.sliderOdLine = [0, this.legendList.oDNumMax] let data = response.value.stationPathDto.splice(0, 100); if (data.length > 0) { data.map((item, index) => { that.odLineData.push({ oDNum: item.oDNum, path: [ [item.startLongitude, item.startLatitude], [item.endLongitude, item.endLatitude] ], startStationName: item.startStationName, endStationName: item.endStationName, }) }) this.isLoading = false; } else { this.isLoading = false; } } } else { this.isLoading = false; console.log(response.message); } }) .catch((error) => { this.isLoading = false; this.$message.error(error); }); },*/ //查询 自定义区域数据(地图及流入流出占比) customAreaData(id) { var that = this; let params = this.searchParams; params.id = id; this.customCircleData.location = []; // 圆区域 this.customPolygonData.location = []; // 多边行区域 if (id !== '') { that.flag = false; API_PASSENGEROD.getAreaInfo(params).then((response) => { if (response.code == 0) { // 圆 if (response.value.areaType == 0) { this.customCircleData.radius = response.value.radius; this.customCircleData.location = [response.value.stationLocationDtoList[0].lng, response.value .stationLocationDtoList[0].lat ]; this.circleMarker(this.customCircleData); // 绘制圆 } else if (response.value.areaType == 1) { // 矩形 this.customPolygonData.areaCoverage = response.value.areaCoverage; let data = response.value.stationLocationDtoList; data.map((item, index) => { this.customPolygonData.location.push( [item.lng, item.lat] ) }) // 绘制矩形 this.polygonMarker(this.customPolygonData); }; this.liquidFillChartInfo = { chartId: "coverAreaRateInfo", chartName: "流入/流出占比", chartValue: [], chartTitle: ['流入率', '流出率'] } if (response.value.inflowProportion == null || response.value.outflowProportion == null) { this.liquidFillChartInfo.chartValue.push(0) this.liquidFillChartInfo.chartValue.push(0) } else { this.liquidFillChartInfo.chartValue.push(response.value.inflowProportion) this.liquidFillChartInfo.chartValue.push(response.value.outflowProportion) } this.infoWindowData = [{ overviewTitle: '区域面积', overviewValue: response.value.areaCoverage, }, { overviewTitle: '流入数量', overviewValue: response.value.inflowPassengers, }, { overviewTitle: '流出数量', overviewValue: response.value.outflowPassengers, } ]; this.$nextTick(() => { that.flag = true; }); } else { this.$message({ message: response.message, type: 'warning' }); } }).catch((error) => { console.log(error) }) } else { this.customCircleData.location = []; this.customPolygonData.location = []; } }, circleMarker(val) { var that = this; // 先清除之前的图形 circleCase && this.map.remove(circleCase) polygonCase && this.map.remove(polygonCase) circleCase = new AMap.Circle({ center: val.location, radius: val.radius, //半径 borderWeight: 3, strokeColor: "#FF33FF", strokeOpacity: 1, strokeWeight: 6, strokeOpacity: 0.2, fillOpacity: 0.4, strokeStyle: 'dashed', strokeDasharray: [10, 10], // 线样式还支持 'dashed' fillColor: '#1791fc', zIndex: 50, }) circleCase.setMap(this.map) that.map.setCenter(val.location) // 缩放地图到合适的视野级别 // this.map.setFitView([circleCase]) circleCase.on("click", function (e) { that.setInfoWindows(e.lnglat); }); }, polygonMarker(val) { var that = this; circleCase && this.map.remove(circleCase) polygonCase && this.map.remove(polygonCase) polygonCase = new AMap.Polygon({ path: val.location, strokeColor: "#FF33FF", strokeWeight: 6, strokeOpacity: 0.2, fillOpacity: 0.4, fillColor: '#1791fc', zIndex: 50, }) this.map.add(polygonCase); polygonCase.on("click", function (e) { that.setInfoWindows(e.lnglat); }); // 缩放地图到合适的视野级别 that.map.setCenter(val.location) // this.map.setFitView([polygonCase]) }, setInfoWindows(e) { var that = this; this.showInfoWindow = true; const infoWindow = new AMap.InfoWindow({ isCustom: true, anchor: "bottom-center", content: this.$refs['infoWindow'].$el, offset: new AMap.Pixel(0, -10), autoMove: true, closeWhenClickMap: true, }); this.stationInfoWindow = infoWindow; this.infoTitle = '基础指标'; // 信息窗口打开 infoWindow.open(that.map, e); }, // 保存后 查询自定义区域地图+ OD线+ 右侧表格+ 图表 seachCustomArea(id, name) { this.editingStatus = true; this.showGraphicsMode = true; this.showCustomMode = true; this.showAllArea = false; this.savaState = false; this.rightMode = true; this.customAreaData(id) // 区域图+ 流入流出占比 this.searchParams.id = id; this.getStationPathOfAreaInfo(this.searchParams, true) // 自定义区域Od线 this.getStationOfAreaInfo(id, true) // 自定义区域表格 this.$refs.searchChild.$emit('childMethod', id, name) // 左侧自定义区域id及名称,保存后改变新增区域状态 }, //选择新增区域类型 checkMode(val) { this.map.clearMap(); this.clearMap(); this.ruleForm.name = ''; this.searchKeys = ''; // this.checkModeIndex = data.label; if (val.label == '图形模式') { if (val.isSelect) { this.showGraphicsMode = false; this.savaState = true; this.rightMode = false; areaPathSimplifierIns && hideAreaLine(); this.mouseTool && this.mouseTool.close(true); circleEditor && circleEditor.close(); this.drawCircleMap() } else { this.showGraphicsMode = true; this.showCustomMode = true; this.savaState = false; this.rightMode = true; areaPathSimplifierIns && showAreaLine(); this.mouseTool && this.mouseTool.close(true); } } else if (val.label == '自定义模式') { if (val.isSelect) { // 选中 this.showCustomMode = false; this.savaState = true; this.rightMode = false; areaPathSimplifierIns && hideAreaLine(); this.mouseTool && this.mouseTool.close(true); polyEditor && polyEditor.close(); this.drawPolygonMap() } else { this.showGraphicsMode = true; this.showCustomMode = true; this.savaState = false; this.rightMode = true; areaPathSimplifierIns && showAreaLine(); this.mouseTool && this.mouseTool.close(true); } } }, // 编辑圆 drawCircleMap() { var that = this; this.mouseTool = new AMap.MouseTool(this.map); this.mouseTool.circle({ strokeColor: "#FF33FF", strokeOpacity: 1, strokeWeight: 6, strokeOpacity: 0.2, fillColor: "#1791fc", fillOpacity: 0.4, strokeStyle: "solid", }); this.mouseTool.on("draw", function (event) { that.radius = event.obj.De.radius.toFixed(0); let centerLng = event.obj.De.center.lng; let centerLat = event.obj.De.center.lat; let position = [centerLng, centerLat]; that.center = [{ lat: centerLat, lng: centerLng }] that.map.clearMap() // 先拿到绘制好的半径圆心再清除进入编辑 var circle = new AMap.Circle({ center: position, radius: that.radius, //半径 borderWeight: 3, strokeColor: "#FF33FF", strokeOpacity: 1, strokeWeight: 6, strokeOpacity: 0.2, fillOpacity: 0.4, strokeStyle: "dashed", strokeDasharray: [10, 10], fillColor: "#1791fc", zIndex: 50, }); circle.setMap(that.map); that.map.setFitView([circle]); circleEditor = new AMap.CircleEditor(that.map, circle); circleEditor.open(); circleEditor.on("move", function (event) {}); circleEditor.on("adjust", function (event) {}); circleEditor.on("end", function (event) { that.radius = event.target.w.radius.toFixed(0); let centerLng = event.target.w.center.lng; let centerLat = event.target.w.center.lat; that.center = [{ lat: centerLat, lng: centerLng }] }); }); }, // 编辑矩形 drawPolygonMap() { var that = this; this.mouseTool = new AMap.MouseTool(this.map); this.mouseTool.polygon({ strokeColor: "#FF33FF", strokeOpacity: 1, strokeWeight: 6, strokeOpacity: 0.2, fillColor: "#1791fc", fillOpacity: 0.4, strokeStyle: "solid", }); this.mouseTool.on("draw", function (event) { that.path = event.obj.w.path; that.map.clearMap(); var polygon = new AMap.Polygon({ path: that.path, strokeColor: "#FF33FF", strokeWeight: 6, strokeOpacity: 0.2, fillOpacity: 0.4, fillColor: '#1791fc', zIndex: 50, }) that.pathArea = polygon.getArea(); // 编辑前的面积 that.map.add(polygon) that.map.setFitView([polygon]); polyEditor = new AMap.PolyEditor(that.map, polygon); polyEditor.open(); polyEditor.on('addnode', function (event) {}) polyEditor.on('adjust', function (event) {}) polyEditor.on('removenode', function (event) {}) polyEditor.on('end', function (event) { that.path = event.target.w.path; // 编辑后面积 that.pathArea = Math.round(AMap.GeometryUtil.ringArea(event.target.w.path)); }) }); }, // 保存 onSubmit() { var that = this; this.$refs.ruleForm.validate((valid) => { if (valid) { if (!that.showGraphicsMode) { // 圆 that.circleOdLine() } else if (!that.showCustomMode) { // 多边形 that.polygonOdLine() } } else { console.log('error submit!!'); return false; } }); }, // 图形模式保存 circleOdLine() { var that = this; let params = { areaName: this.ruleForm.name, areaType: 0, // 区域类型 radius: this.radius, location: this.center, } API_PASSENGEROD.saveAreaInfo(params).then((response) => { if (response.code == 0) { this.map.clearMap(); let id = response.value; this.mouseTool.close(); circleEditor && circleEditor.close(); this.seachCustomArea(id, this.ruleForm.name) } else { this.$message({ message: response.message, type: 'warning' }); } }).catch((error) => { this.$message({ message: error, type: 'warning' }); }) }, // 自定义模式保存 polygonOdLine() { let location = []; this.path.map((item, index) => { location.push({ lat: item.lat, lng: item.lng }) }) let params = { areaName: this.ruleForm.name, areaType: 1, // 区域类型 location: location, areaCoverage: this.pathArea, } API_PASSENGEROD.saveAreaInfo(params).then((response) => { if (response.code == 0) { this.map.clearMap(); let id = response.value; this.mouseTool.close(); polyEditor && polyEditor.close(); this.seachCustomArea( id, this.ruleForm.name); } else { this.$message({ message: response.message, type: 'warning' }); } }).catch((error) => { this.$message({ message: error, type: 'warning' }); }) }, //右侧全城区域top10表格 getRoadOD(data) { let params = { startTime: data.startTime, endTime: data.endTime, peakSection: data.peakSection, isWeekday: data.isWeekday, top: 10, } API_PASSENGEROD.getRegionalTravelOD(params).then(res => { if (res.code == 0 && res.value.length != 0) { this.odTableList = res.value } else { this.odTableList = []; this.$message.error('未查询到 线路OD Top10 数据') } }) }, // 站点od getStationMap(data,status,isStationId){ this.isLoading = true; let params = {}; params.startTime = data.startTime; params.endTime = data.endTime; params.byLine =false; params.top = 100; params.ODNumBefore =this.sliderOdLine[0]; params.ODNumAfter =this.sliderOdLine[1]; if(isStationId == 0) { params.platformIdOn =''; params.platformIdOn =''; params.peakSection =''; params.isWeekday= ''; } else { params.platformIdOn=data.platformIdOn; params.platformIdOff=data.platformIdOff; params.peakSection=data.peakSection; params.isWeekday=data.isWeekday; } this.stationMapData = []; API_PASSENGEROD.getODNum3(params).then(res => { if(res.code == 0) { this.legendList.oDNumMin = res.value.oDNumMin; this.legendList.oDNumMid = res.value.oDNumMid; this.legendList.oDNumMax = res.value.oDNumMax; let keyData = [0, this.legendList.oDNumMin, this.legendList.oDNumMid, this.legendList.oDNumMax]; let valData = [String(0), String(this.legendList.oDNumMin), String(this.legendList.oDNumMid), String( this.legendList.oDNumMax)] let obj = {}; for (let i = 0; i < valData.length; i++) { const key = keyData[i]; obj[key] = valData[i] } this.marks = obj; if(status){ this.sliderOdLine = [0, this.legendList.oDNumMax] } this.stationMapData = res.value this.drawStationMap(); } else { this.isLoading = false; console.log(res.message) } }).catch((error)=>{ this.isLoading = false; console.log(error.msg) }) }, //绘制飞线图 drawStationMap() { var that = this; if(stationPathSimplifierIns !== null){ showStations(); } else { AMapUI.loadUI( ["misc/PathSimplifier", "misc/PointSimplifier"], (PathSimplifier, PointSimplifier) => { if (!PathSimplifier.supportCanvas) { alert("当前环境不支持 Canvas!"); return; } // od线 let oDMaxList = this.stationMapData.oDMaxList let oDMidList = this.stationMapData.oDMidList let oDMinList = this.stationMapData.oDMinList let red = this.stationMapData.red let yellow = this.stationMapData.yellow let arr = [oDMaxList,oDMidList,oDMinList] let iconList = [red,yellow] that.renderLayerMark(arr,iconList) }); } }, // 默认线路 defaultLineOD(data,status){ lineStationPathSimplifierIns && hideStationLine(); this.isLoading = true; let params = { startTime: data.startTime, endTime: data.endTime, ODNumBefore:this.sliderOdLine[0], ODNumAfter:this.sliderOdLine[1], byLine:true, top:100, } this.lineMapData = []; API_PASSENGEROD.getODNum3(params).then(res => { if(res.code == 0) { this.legendList.oDNumMin = res.value.oDNumMin; this.legendList.oDNumMid = res.value.oDNumMid; this.legendList.oDNumMax = res.value.oDNumMax; let keyData = [0, this.legendList.oDNumMin, this.legendList.oDNumMid, this.legendList.oDNumMax]; let valData = [String(0), String(this.legendList.oDNumMin), String(this.legendList.oDNumMid), String( this.legendList.oDNumMax)] let obj = {}; for (let i = 0; i < valData.length; i++) { const key = keyData[i]; obj[key] = valData[i] } this.marks = obj; if(status){ this.sliderOdLine = [0, this.legendList.oDNumMax] } this.sliderOdLine = res.value this.drawLineMap(); } else { this.isLoading = false; console.log(res.message) } }).catch((error)=>{ this.isLoading = false; console.log(error.msg) }) }, drawLineMap() { var that = this; if(linePathSimplifierIns !== null) { showLine(); } else { AMapUI.loadUI( ["misc/PathSimplifier", "misc/PointSimplifier"], (PathSimplifier, PointSimplifier) => { if (!PathSimplifier.supportCanvas) { alert("当前环境不支持 Canvas!"); return; } let oDMaxList = this.sliderOdLine.oDMaxList let oDMidList = this.sliderOdLine.oDMidList let oDMinList = this.sliderOdLine.oDMinList let red = this.sliderOdLine.red let yellow = this.sliderOdLine.yellow let arr = [oDMaxList,oDMidList,oDMinList] let iconList = [red,yellow] that.renderLayerMark(arr,iconList) }); } }, // 选中线路 checkLineOD(data){ this.sliderDisabled = true; // 先清除之前线路 if(this.stationMarkerArr.length > 0) { this.stationMarkerArr.map((item,index)=>{ this.map.remove(item); }) } this.map.clearMap(); this.stationMarkerArr = []; this.checkLineObj.lineStationInfo = []; let params = { lineId:data.lineId, direction:data.direction , startTime: data.startTime, endTime: data.endTime, peakSection:data.peakSection, isWeekday:data.isWeekday, } API_PASSENGEROD.getLinePassengerODBaseInfo(params).then(res => { if(res.code == 0) { console.log(layer1,'layer1') if(layer1){ layer1.setMap(null); layer2.setMap(null); layer3.setMap(null); } if(markIcon1){ markIcon1.setMap(null); markIcon2.setMap(null); } let data = res.value; this.checkLineObj.linePath = JSON.parse(data.linePath); data.lineStationInfo.map((item,index)=>{ let obj = {}; obj.center = [item.longitude,item.latitude]; obj.name = item.stationName; obj.stationId = item.stationId; this.checkLineObj.lineStationInfo.push(obj); }) this.checkLineObj.lineName = data.lineName; this.checkLineObj.passengerNum = data.passengerNum; this.checkLineObj.stationNum = data.stationNum; this.checkLineObj.timeImbalanceFactor = data.timeImbalanceFactor; this.$nextTick(()=>{ this.getcheckLineMap(this.checkLineObj.linePath,this.checkLineObj.lineStationInfo); }) } else { console.log(res.message) } }).catch((error)=>{ console.log(error.msg) }) }, getcheckLineMap(path,stations){ var that = this; // 站点 let iconStyle = new AMap.Icon({ size: new AMap.Size(15, 15), image: require("@/assets/images/layout/selected-vehicle.png"), imageSize: new AMap.Size(15, 15), }); this.stationMarkerArr = []; stations.forEach((marker) => { var stationMarker= new AMap.Marker({ icon: iconStyle, map: this.map, offset: new AMap.Pixel(-8, -8), position: [marker.center[0], marker.center[1]], }); stationMarker.setLabel({ offset: new AMap.Pixel(5, 20), //设置文本标注偏移量 content: marker.name, direction: "right", //设置文本标注方位 }); that.stationMarkerArr.push(stationMarker); // 点击显示站点od stationMarker.on("click", () => { this.searchParams.stationId = marker.stationId; this.getLineStationPassengersOD(this.searchParams,true,0) }); }); // 线路 // 定义线的风格 var polyline = new AMap.Polyline({ path: path, isOutline: true, outlineColor: "#49C3FC", borderWeight: 1, strokeColor: "#49C3FC", strokeOpacity: 1, strokeWeight: 2, strokeStyle: "solid", strokeDasharray: [10, 5], lineJoin: "round", lineCap: "round", zIndex: 10, }); polyline.setMap(this.map); // 线路 // 路线的起始点 var startMarker = new AMap.Marker({ position: path[0], icon: "https://webapi.amap.com/theme/v1.3/markers/n/start.png", map: this.map, }); var endMarker = new AMap.Marker({ position: path[path.length - 1], icon: "https://webapi.amap.com/theme/v1.3/markers/n/end.png", map: this.map, }); startMarker.setMap(this.map); endMarker.setMap(this.map); // 缩放地图到合适的视野级别 this.map.setFitView([polyline, startMarker, endMarker]); }, // 线路站点基本信息 getLineStationPassengersOD(val,status,initStatus){ // 首次点击无od量 this.isLoading = true; this.sliderDisabled = false; let params = {}; params.lineId = val.lineId; params.direction = val.direction; params.stationId = val.stationId; params.startTime = val.startTime; params.endTime = val.endTime; params.peakSection = val.peakSection; params.isWeekday = val.isWeekday; if(initStatus == 0){ params.ODNumBefore = ''; params.ODNumAfter = ''; } else { params.ODNumBefore =this.sliderOdLine[0]; params.ODNumAfter = this.sliderOdLine[1]; } this.checkStationOd = []; API_PASSENGEROD.getLineStationPassengersOD2(params).then(res => { if(res.code == 0) { this.legendList.oDNumMin = res.value.oDNumMin; this.legendList.oDNumMid = res.value.oDNumMid; this.legendList.oDNumMax = res.value.oDNumMax; let keyData = [0, this.legendList.oDNumMin, this.legendList.oDNumMid, this.legendList.oDNumMax]; let valData = [String(0), String(this.legendList.oDNumMin), String(this.legendList.oDNumMid), String( this.legendList.oDNumMax)] let obj = {}; for (let i = 0; i < valData.length; i++) { const key = keyData[i]; obj[key] = valData[i] } this.marks = obj; if(status){ this.sliderOdLine = [0, this.legendList.oDNumMax] } this.checkStationOd = res.value this.$nextTick(()=>{ this.checkStationOdMap(); }) this.isLoading = false; } else { console.log(res.message); this.isLoading = false; } }).catch((error)=>{ console.log(error.msg); this.isLoading = false; }) }, // 选择线路站点 checkStationOdMap(){ var that = this; if(lineStationPathSimplifierIns !== null) { showStationLine() } else { AMapUI.loadUI( ["misc/PathSimplifier", "misc/PointSimplifier"], (PathSimplifier, PointSimplifier) => { if (!PathSimplifier.supportCanvas) { alert("当前环境不支持 Canvas!"); return; } let oDMaxList = this.checkStationOd.oDMaxList let oDMidList = this.checkStationOd.oDMidList let oDMinList = this.checkStationOd.oDMinList let red = this.checkStationOd.red let yellow = this.checkStationOd.yellow let arr = [oDMaxList,oDMidList,oDMinList] let iconList = [red,yellow] that.renderLayerMark(arr,iconList) }); } }, /** * @Description: 创建飞线图及高亮站点 * @author konghaitao * @date 2023-2-20 * @params arr 飞线图数组 iconList 图标数组 */ renderLayerMark(arr,iconList){ //倒叙创建覆盖 let that = this let colors = [ "#FF0000", "#FFC402", "#4E79E9", ]; let icons = [this.redIcon,this.yellowIcon] that.clearMap() layer3 = new Loca.LinkLayer({ map: that.map, fitView: false, }); layer3.setData(arr[2], { lnglat: 'lnglat' }); layer3.setOptions({ style: { // 曲率 [-1, 1] 区间 curveness: function(data) { if(data.value.dis < 300){ return 0.005; } else { return 0.001; } }, borderWidth: 1.5, width: 1, opacity: 0.8, color: colors[2] } }); layer2 = new Loca.LinkLayer({ map: that.map, fitView: false, }); layer2.setData(arr[1], { lnglat: 'lnglat' }); layer2.setOptions({ style: { // 曲率 [-1, 1] 区间 curveness: function(data) { if(data.value.dis < 300){ return 0.005; } else { return 0.001; } }, width: 1, opacity: 0.8, color: colors[1] } }); layer1 = new Loca.LinkLayer({ map: that.map, fitView: false, }); layer1.setData(arr[0], { lnglat: 'lnglat' }); layer1.setOptions({ style: { // 曲率 [-1, 1] 区间 curveness: function(data) { if(data.value.dis < 300){ return 0.005; } else { return 0.001; } }, width: 1, opacity: 0.8, color: colors[0] } }); layer1.render(); layer2.render(); layer3.render(); if(iconList){ markIcon2 = new Loca.IconLayer({ map: that.map, fitView: true, }); markIcon2.setData(iconList[1], { lnglat: 'center' }); markIcon2.setOptions({ source: function (res) { return icons[1]; }, style: { size: 28, } }); markIcon2.render(); markIcon1 = new Loca.IconLayer({ map: that.map, fitView: true, }); markIcon1.setData(iconList[0], { lnglat: 'center' }); markIcon1.setOptions({ source: function (res) { return icons[0]; }, style: { size: 28, } }); markIcon1.render(); } that.$nextTick(()=>{ that.isLoading = false; }) }, changeFlowEvent(data) { // let params = this.searchParams; switch (this.nowIndex) { case 0: this.getStationOD() break case 1: break case 2: this.getStationOfAreaInfo(this.searchId, data); this.getStationPathOfAreaInfo(this.searchId, data) // 自定义区域Od线 break } }, getStationOfAreaInfo(id, data) { let params = this.searchParams; params.id = id; params.flowChoose = data; params.top = 1000; this.searchId = id; API_PASSENGEROD .getStationOfAreaInfo(params) .then((response) => { if (response.code === 0) { this.odTableList = response.value } else { console.log(response.message); } }) .catch((error) => { this.$message.error(error); }); }, getStationPathOfAreaInfo(data, status) { console.log(data.id,'aaaa') var that = this; this.odLineData = []; this.isLoading = true; let params = { id:data.id, startTime:data.startTime, endTime:data.endTime, peakSection:data.peakSection, isWeekday:data.isWeekday, flowChoose:data.flowChoose, ODNumBefore: this.sliderOdLine[0], ODNumAfter:this.sliderOdLine[1], } API_PASSENGEROD .getStationPathOfAreaInfo2(params) .then((response) => { if (response.code === 0) { this.legendList.oDNumMin = response.value.oDNumMin; this.legendList.oDNumMid = response.value.oDNumMid; this.legendList.oDNumMax = response.value.oDNumMax; let keyData = [0, this.legendList.oDNumMin, this.legendList.oDNumMid, this.legendList.oDNumMax]; let valData = [String(0), String(this.legendList.oDNumMin), String(this.legendList.oDNumMid), String( this.legendList.oDNumMax)] let obj = {}; for (let i = 0; i < valData.length; i++) { const key = keyData[i]; obj[key] = valData[i] } this.marks = obj; if(status) { this.sliderOdLine = [0, this.legendList.oDNumMax] } that.odLineData = response.value this.areaOdLineMap(); } else { this.isLoading = false; console.log(response.message); } }) .catch((error) => { this.isLoading = false; this.$message.error(error); }); }, // 区域od线 areaOdLineMap(){ var that = this; if(areaPathSimplifierIns !== null){ showAreaLine(); } else { AMapUI.load(['ui/misc/PathSimplifier', 'lib/$'], function (PathSimplifier, $) { if (!PathSimplifier.supportCanvas) { alert('当前环境不支持 Canvas!'); return; } let oDMaxList = that.odLineData.oDMaxList let oDMidList = that.odLineData.oDMidList let oDMinList = that.odLineData.oDMinList let arr = [oDMaxList,oDMidList,oDMinList] that.renderLayerMark(arr) }); } }, // 初始化地图 initMap() { lazyAMapApiLoaderInstance.load().then(() => { this.map = new AMap.Map(document.getElementById("container"), { mapStyle: "amap://styles/9824f612cff8dc63104cff89ddfd1a93", expandZoomRange: true, zoom: 13, zooms: [3, 21], resizeEnable: false, center: [117.38, 32.92], keyboardEnable: false, zoomEnable: true, dragEnable: true, animateEnable: true, viewMode: '3D', pitch: 30, }); }); }, }, mounted() { this.$bus.$on('checkMode', (data) => { this.checkMode(data); if(data.type == 1){ //type为1 新增区域模块 this.editingStatus = !data.isSelect; } }) console.log('aaaaaaaaaaaa') this.initMap(); }, created() { }, beforeDestroy() { this.$bus.$off("checkMode"); this.map = null; stationPathSimplifierIns = null; linePathSimplifierIns = null; lineStationPathSimplifierIns = null; areaPathSimplifierIns = null; this.clearMap() }, } </script> <style lang="scss" scoped> // PX 转 rem @function px2Rem($px, $base-font-size: 19.2px) { @if (unitless($px)) { //有无单位 @return ($px / 19.2) * 1rem; } @else if (unit($px)==em) { @return $px; } @return ($px / $base-font-size) * 1rem; } .passengerflowOd-wrap { .left-wrap { .modeList { float: left; position: relative; } .mode { width: px2Rem(150px); height: px2Rem(40px); text-align: center; line-height: px2Rem(40px); background-color: #fff; margin-right: px2Rem(10px); } .mask { position: absolute; left: 0px; top: 0px; width: px2Rem(150px); height: px2Rem(40px); border: 2px solid #2894ec; } } // 图例 .legend-map { position: absolute; bottom: px2Rem(55px); right: 28%; width: px2Rem(80px); height: px2Rem(100px); display: flex; justify-content: space-between; flex-direction: column; color: #fff; background: #001539; box-shadow: 0px 0px 8px 2px rgba(16, 108, 222, 0.8); border-radius: 4px; opacity: 0.8; z-index: 100; .item { height: px2Rem(30px); display: flex; flex-direction: row; // justify-content: start; justify-content: space-around; align-items: center; font-size: px2Rem(14px); margin: 0 15px 0px; .base-color { width: px2Rem(15px); height: px2Rem(6px); border-radius: 2px; } .color-0 { background: #FF0000; } .color-1 { background: #FFC402; } .color-2 { background: #4E79E9; } } } .od-line { position: absolute; bottom: px2Rem(55px); left: 28%; z-index: 100; font-size: px2Rem(18px); color: #fff; .od-slider { margin-top: px2Rem(10px); padding: px2Rem(7px) px2Rem(15px); width: px2Rem(250px); height: px2Rem(60px); background: #001539; box-shadow: 0px 0px 8px 2px rgba(16, 108, 222, 0.8); border-radius: 4px; z-index: 100; } } .siteSearch { position: absolute; top: px2Rem(100px); right: 28%; z-index: 100; } .areaName { position: absolute; bottom: px2Rem(55px); right: 28%; z-index: 100; } } </style> <style lang="scss"> // PX 转 rem @function px2Rem($px, $base-font-size: 19.2px) { @if (unitless($px)) { //有无单位 @return ($px / 19.2) * 1rem; } @else if (unit($px)==em) { @return $px; } @return ($px / $base-font-size) * 1rem; } // 站点名样式 .amap-marker-label { position: absolute; border: none; background-color: transparent; white-space: nowrap; cursor: pointer; padding: 3px; font-size: 9px; font-family: AlibabaPuHuiTiR; color: #ffffff; // l } </style> 分析当前代码,首次切换模块打印new Loca.LinKLayer报错,强制刷新后Loca功能才可使用,排查下原因

我现在有一个空项目,前端使用vite+vue3+vue-router+pinia+element-plus+axios开发,现在需要你帮忙生成几个页面。 首先第一个页面是一个项目的介绍页,要好看点,且能突出主题。我们是一个用户填报简历就可以和我们的岗位库进行对比然后生成分析的网站。首页要大气点,而且要突出主题,网站主题叫《职景Vision》,slogan为:一个为求职者提供岗位推荐及前景分析的可视化智能平台,拥有30W+岗位数据,只需几秒即可查看岗位前景。同时在该页面下部要加一个按钮,按钮文字为“立马开始”,点击就可以进入第二个页面。 第二个页面应该有一个进度条,在最顶端。进来后进度条为:填写您的简历。后面是“生成分析中”,最后是“分析报告“,进度条应该是这三部分。进入该页面直接是一个填报表单,让用户填报简历信息,具体字段参考建表语句。 表单应包含如下字段、岗位(下拉单选框,备选项你可以自己先定义几个)、城市(文本框)、学历(文本框)、技能专长(文本域控件)、工作经历(文本域)、项目经历(文本域)、工作经验(文本框)、期望薪资(下拉单选,备选项你根据经验先设置几个) CREATE TABLE resumes ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, full_name VARCHAR(100), contact VARCHAR(50), education TEXT, work_experience TEXT, skills TEXT, projects TEXT, file_path VARCHAR(255), embedding BLOB, created_at TIMESTAMP ); 点击填报完成按钮,进度条就会走向第二个生成分析中,此页面要加一个加载动画,最后加载完毕后再跳转到分析报告页面。

zip
JFM7VX690T型SRAM型现场可编程门阵列技术手册主要介绍的是上海复旦微电子集团股份有限公司(简称复旦微电子)生产的高性能FPGA产品JFM7VX690T。该产品属于JFM7系列,具有现场可编程特性,集成了功能强大且可以灵活配置组合的可编程资源,适用于实现多种功能,如输入输出接口、通用数字逻辑、存储器、数字信号处理和时钟管理等。JFM7VX690T型FPGA适用于复杂、高速的数字逻辑电路,广泛应用于通讯、信息处理、工业控制、数据中心、仪表测量、医疗仪器、人工智能、自动驾驶等领域。 产品特点包括: 1. 可配置逻辑资源(CLB),使用LUT6结构。 2. 包含CLB模块,可用于实现常规数字逻辑和分布式RAM。 3. 含有I/O、BlockRAM、DSP、MMCM、GTH等可编程模块。 4. 提供不同的封装规格和工作温度范围的产品,便于满足不同的使用环境。 JFM7VX690T产品系列中,有多种型号可供选择。例如: - JFM7VX690T80采用FCBGA1927封装,尺寸为45x45mm,使用锡银焊球,工作温度范围为-40°C到+100°C。 - JFM7VX690T80-AS同样采用FCBGA1927封装,但工作温度范围更广,为-55°C到+125°C,同样使用锡银焊球。 - JFM7VX690T80-N采用FCBGA1927封装和铅锡焊球,工作温度范围与JFM7VX690T80-AS相同。 - JFM7VX690T36的封装规格为FCBGA1761,尺寸为42.5x42.5mm,使用锡银焊球,工作温度范围为-40°C到+100°C。 - JFM7VX690T36-AS使用锡银焊球,工作温度范围为-55°C到+125°C。 - JFM7VX690T36-N使用铅锡焊球,工作温度范围与JFM7VX690T36-AS相同。 技术手册中还包含了一系列详细的技术参数,包括极限参数、推荐工作条件、电特性参数、ESD等级、MSL等级、重量等。在产品参数章节中,还特别强调了封装类型,包括外形图和尺寸、引出端定义等。引出端定义是指对FPGA芯片上的各个引脚的功能和接线规则进行说明,这对于FPGA的正确应用和电路设计至关重要。 应用指南章节涉及了FPGA在不同应用场景下的推荐使用方法。其中差异说明部分可能涉及产品之间的性能差异;关键性能对比可能包括功耗与速度对比、上电浪涌电流测试情况说明、GTH Channel Loss性能差异说明、GTH电源性能差异说明等。此外,手册可能还提供了其他推荐应用方案,例如不使用的BANK接法推荐、CCLK信号PCB布线推荐、JTAG级联PCB布线推荐、系统工作的复位方案推荐等,这些内容对于提高系统性能和稳定性有着重要作用。 焊接及注意事项章节则针对产品的焊接过程提供了指导,强调焊接过程中的注意事项,以确保产品在组装过程中的稳定性和可靠性。手册还明确指出,未经复旦微电子的许可,不得翻印或者复制全部或部分本资料的内容,且不承担采购方选择与使用本文描述的产品和服务的责任。 上海复旦微电子集团股份有限公司拥有相关的商标和知识产权。该公司在中国发布的技术手册,版权为上海复旦微电子集团股份有限公司所有,未经许可不得进行复制或传播。 技术手册提供了上海复旦微电子集团股份有限公司销售及服务网点的信息,方便用户在需要时能够联系到相应的服务机构,获取最新信息和必要的支持。同时,用户可以访问复旦微电子的官方网站(***以获取更多产品信息和公司动态。

大家在看

recommend-type

Xilinx ISE rs_decoder_ipcore and encoder License

Xilinx ISE RS编码解码IP核的License
recommend-type

毕业设计&课设-一个基于Matlab的PET仿真和重建框架,具有系统矩阵的分析建模,能够结合各种数据….zip

matlab算法,工具源码,适合毕业设计、课程设计作业,所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答! matlab算法,工具源码,适合毕业设计、课程设计作业,所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答! matlab算法,工具源码,适合毕业设计、课程设计作业,所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答! matlab算法,工具源码,适合毕业设计、课程设计作业,所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答! matlab算法,工具源码,适合毕业设计、课程设计作业,所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答! matlab算法,工具源码,适合毕业设计、课程设计作业,所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随
recommend-type

MATLAB机械臂简单控制仿真(Simulink篇-总).zip

MATLAB下机器人可视化与控制---simulink篇中的简单例子,在Simulink中做了预定义轨迹的运动和Slider Gain控制的运动,用GUI控制的关节代码在MATLAB下机器人可视化与控制
recommend-type

使用 GCC 构建 STM23F0 ARM 项目的模板源码

使用 GCC 构建 STM23F0 ARM 项目的模板源码,具体请看 README
recommend-type

详细说明 VC++的MFC开发串口调试助手源代码,包括数据发送,接收,显示制式等29782183com

详细说明 VC++的MFC开发串口调试助手源代码,包括数据发送,接收,显示制式等29782183com

最新推荐

recommend-type

计算机网络学习中学员常见问题与改进方法

计算机网络学习中学员常见问题与改进方法+
recommend-type

基于高斯混合模型(GMM)和主成分分析(PCA)的疲劳语音识别.zip

1.版本:matlab2014a/2019b/2024b 2.附赠案例数据可直接运行。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
recommend-type

Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目)

Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目),个人大四的毕业设计、经导师指导并认可通过的高分设计项目,评审分99分,代码完整确保可以运行,小白也可以亲自搞定,主要针对计算机相关专业的正在做毕设的学生和需要项目实战练习的学习者,也可作为课程设计、期末大作业。 Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目)Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目)Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目)Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目)Java毕业设计基于SpringBoot+Vue开发的智慧农业系统源码+数据库(高分项目)Java毕业设计个人大四的毕业设计、经导师指导并认可通过的高分设计项目,评审分99分,代码完整确保可以运行,小白也可以亲自搞定,主要针对计算机相关专业的正在做毕设的学生和需要项目实战练习的学习者,也可作为课程设计、期末大作业。个人大四的毕业设计 收起
recommend-type

用bp神经网络预测油田产量

资源下载链接为: https://pan.quark.cn/s/82cb66f4d6b4 用bp神经网络预测油田产量(最新、最全版本!打开链接下载即可用!)
recommend-type

2_JFM7VX690T型SRAM型现场可编程门阵列技术手册.pdf

JFM7VX690T型SRAM型现场可编程门阵列技术手册主要介绍的是上海复旦微电子集团股份有限公司(简称复旦微电子)生产的高性能FPGA产品JFM7VX690T。该产品属于JFM7系列,具有现场可编程特性,集成了功能强大且可以灵活配置组合的可编程资源,适用于实现多种功能,如输入输出接口、通用数字逻辑、存储器、数字信号处理和时钟管理等。JFM7VX690T型FPGA适用于复杂、高速的数字逻辑电路,广泛应用于通讯、信息处理、工业控制、数据中心、仪表测量、医疗仪器、人工智能、自动驾驶等领域。 产品特点包括: 1. 可配置逻辑资源(CLB),使用LUT6结构。 2. 包含CLB模块,可用于实现常规数字逻辑和分布式RAM。 3. 含有I/O、BlockRAM、DSP、MMCM、GTH等可编程模块。 4. 提供不同的封装规格和工作温度范围的产品,便于满足不同的使用环境。 JFM7VX690T产品系列中,有多种型号可供选择。例如: - JFM7VX690T80采用FCBGA1927封装,尺寸为45x45mm,使用锡银焊球,工作温度范围为-40°C到+100°C。 - JFM7VX690T80-AS同样采用FCBGA1927封装,但工作温度范围更广,为-55°C到+125°C,同样使用锡银焊球。 - JFM7VX690T80-N采用FCBGA1927封装和铅锡焊球,工作温度范围与JFM7VX690T80-AS相同。 - JFM7VX690T36的封装规格为FCBGA1761,尺寸为42.5x42.5mm,使用锡银焊球,工作温度范围为-40°C到+100°C。 - JFM7VX690T36-AS使用锡银焊球,工作温度范围为-55°C到+125°C。 - JFM7VX690T36-N使用铅锡焊球,工作温度范围与JFM7VX690T36-AS相同。 技术手册中还包含了一系列详细的技术参数,包括极限参数、推荐工作条件、电特性参数、ESD等级、MSL等级、重量等。在产品参数章节中,还特别强调了封装类型,包括外形图和尺寸、引出端定义等。引出端定义是指对FPGA芯片上的各个引脚的功能和接线规则进行说明,这对于FPGA的正确应用和电路设计至关重要。 应用指南章节涉及了FPGA在不同应用场景下的推荐使用方法。其中差异说明部分可能涉及产品之间的性能差异;关键性能对比可能包括功耗与速度对比、上电浪涌电流测试情况说明、GTH Channel Loss性能差异说明、GTH电源性能差异说明等。此外,手册可能还提供了其他推荐应用方案,例如不使用的BANK接法推荐、CCLK信号PCB布线推荐、JTAG级联PCB布线推荐、系统工作的复位方案推荐等,这些内容对于提高系统性能和稳定性有着重要作用。 焊接及注意事项章节则针对产品的焊接过程提供了指导,强调焊接过程中的注意事项,以确保产品在组装过程中的稳定性和可靠性。手册还明确指出,未经复旦微电子的许可,不得翻印或者复制全部或部分本资料的内容,且不承担采购方选择与使用本文描述的产品和服务的责任。 上海复旦微电子集团股份有限公司拥有相关的商标和知识产权。该公司在中国发布的技术手册,版权为上海复旦微电子集团股份有限公司所有,未经许可不得进行复制或传播。 技术手册提供了上海复旦微电子集团股份有限公司销售及服务网点的信息,方便用户在需要时能够联系到相应的服务机构,获取最新信息和必要的支持。同时,用户可以访问复旦微电子的官方网站(***以获取更多产品信息和公司动态。
recommend-type

美国国际航空交通数据分析报告(1990-2020)

根据给定的信息,我们可以从中提取和分析以下知识点: 1. 数据集概述: 该数据集名为“U.S. International Air Traffic data(1990-2020)”,记录了美国与国际间航空客运和货运的详细统计信息。数据集涵盖的时间范围从1990年至2020年,这说明它包含了长达30年的时间序列数据,对于进行长期趋势分析非常有价值。 2. 数据来源及意义: 此数据来源于《美国国际航空客运和货运统计报告》,该报告是美国运输部(USDOT)所管理的T-100计划的一部分。T-100计划旨在收集和发布美国和国际航空公司在美国机场的出入境交通报告,这表明数据的权威性和可靠性较高,适用于政府、企业和学术研究等领域。 3. 数据内容及应用: 数据集包含两个主要的CSV文件,分别是“International_Report_Departures.csv”和“International_Report_Passengers.csv”。 a. International_Report_Departures.csv文件可能包含了以下内容: - 离港航班信息:记录了各航空公司的航班号、起飞和到达时间、起飞和到达机场的代码以及国际地区等信息。 - 航空公司信息:可能包括航空公司代码、名称以及所属国家等。 - 飞机机型信息:如飞机类型、座位容量等,这有助于分析不同机型的使用频率和趋势。 - 航线信息:包括航线的起始和目的国家及城市,对于研究航线网络和优化航班计划具有参考价值。 这些数据可以用于航空交通流量分析、机场运营效率评估、航空市场分析等。 b. International_Report_Passengers.csv文件可能包含了以下内容: - 航班乘客信息:可能包括乘客的国籍、年龄、性别等信息。 - 航班类型:如全客机、全货机或混合型航班,可以分析乘客运输和货物运输的比例。 - 乘客数量:记录了各航班或航线的乘客数量,对于分析航空市场容量和增长趋势很有帮助。 - 飞行里程信息:有助于了解国际间不同航线的长度和飞行距离,为票价设置和燃油成本分析提供数据支持。 这些数据可以用于航空客运市场分析、需求预测、收益管理等方面。 4. 数据分析和应用实例: - 航空流量分析:通过分析离港航班数据,可以观察到哪些航线最为繁忙,哪些机场的国际航空流量最大,这有助于航空公司调整航班时刻表和运力分配。 - 市场研究:乘客数据可以揭示不同国家和地区之间的人口流动趋势,帮助航空公司和政府机构了解国际旅行市场的需求变化。 - 飞机利用率:结合飞机机型和飞行频率信息,可以对特定机型的使用率进行分析,评估飞机维护需求和燃油效率。 - 安全监管:通过对比不同航空公司和航班的安全记录,监管机构可以更有效地评估航空公司的安全性能,并采取必要的监管措施。 5. 技术和方法论: 分析此类数据通常涉及数据清洗、数据整合、统计分析、时间序列分析、预测建模等数据科学方法。使用Excel、SQL、R、Python等工具进行数据处理和分析是常见的做法。例如,可以使用Python的Pandas库来清洗和准备数据,使用Matplotlib和Seaborn库来可视化数据,然后利用Scikit-learn或Statsmodels库来构建预测模型。 通过以上知识点的提取和分析,我们可以理解到“U.S. International Air Traffic data(1990-2020)-数据集”的重要性,它不仅记录了跨越30年的航空交通数据,还为各种分析和应用提供了详实的基础信息。对于航空业从业者、政策制定者、研究人员以及数据分析师来说,这是一个极具价值的数据资源。
recommend-type

统计学视角:深入理解最小二乘法的概率论基础

# 1. 最小二乘法的基本概念 最小二乘法(Least Squares Method, LSM)是统计学和数据分析中广泛使用的一种数学优化技术。其主要目的是通过最小化误差的平方和来寻找数据的最佳函数匹配。这种方法最早由高斯提出,但在工程、物理、经济和其他领域中有着广泛的应用。 在本章中,我们将首先了
recommend-type

vscode中使用Codeium

<think>好的,我需要回答用户如何在VSCode中使用Codeium插件的问题。首先,我需要回顾用户提供的引用内容,看看是否有相关信息。用户提供的引用[1]提到了Codeium Chat可以解释代码中的问题,引用[2]提到了在IDEA中的一些问题,可能对VSCode的安装有帮助。用户还提到了安装步骤、基本功能、常见问题等。 首先,我应该分步骤说明安装过程,包括在VSCode扩展商店搜索Codeium并安装。然后,登录部分可能需要用户访问仪表板获取API密钥,引用[2]中提到登录问题,可能需要提醒用户注意网络或权限设置。 接下来是基本功能,比如代码自动补全和Chat功能。引用[1]提到C
recommend-type

UniMoCo:统一框架下的多监督视觉学习方法

在详细解析“unimoco”这个概念之前,我们需要明确几个关键点。首先,“unimoco”代表的是一种视觉表示学习方法,它在机器学习尤其是深度学习领域中扮演着重要角色。其次,文章作者通过这篇论文介绍了UniMoCo的全称,即“Unsupervised, Semi-Supervised and Full-Supervised Visual Representation Learning”,其背后的含义是在于UniMoCo框架整合了无监督学习、半监督学习和全监督学习三种不同的学习策略。最后,该框架被官方用PyTorch库实现,并被提供给了研究者和开发者社区。 ### 1. 对比学习(Contrastive Learning) UniMoCo的概念根植于对比学习的思想,这是一种无监督学习的范式。对比学习的核心在于让模型学会区分不同的样本,通过将相似的样本拉近,将不相似的样本推远,从而学习到有效的数据表示。对比学习与传统的分类任务最大的不同在于不需要手动标注的标签来指导学习过程,取而代之的是从数据自身结构中挖掘信息。 ### 2. MoCo(Momentum Contrast) UniMoCo的实现基于MoCo框架,MoCo是一种基于队列(queue)的对比学习方法,它在训练过程中维持一个动态的队列,其中包含了成对的负样本。MoCo通过 Momentum Encoder(动量编码器)和一个队列来保持稳定和历史性的负样本信息,使得模型能够持续地进行对比学习,即使是在没有足够负样本的情况下。 ### 3. 无监督学习(Unsupervised Learning) 在无监督学习场景中,数据样本没有被标记任何类别或标签,算法需自行发现数据中的模式和结构。UniMoCo框架中,无监督学习的关键在于使用没有标签的数据进行训练,其目的是让模型学习到数据的基础特征表示,这对于那些标注资源稀缺的领域具有重要意义。 ### 4. 半监督学习(Semi-Supervised Learning) 半监督学习结合了无监督和有监督学习的优势,它使用少量的标注数据与大量的未标注数据进行训练。UniMoCo中实现半监督学习的方式,可能是通过将已标注的数据作为对比学习的一部分,以此来指导模型学习到更精准的特征表示。这对于那些拥有少量标注数据的场景尤为有用。 ### 5. 全监督学习(Full-Supervised Learning) 在全监督学习中,所有的训练样本都有相应的标签,这种学习方式的目的是让模型学习到映射关系,从输入到输出。在UniMoCo中,全监督学习用于训练阶段,让模型在有明确指示的学习目标下进行优化,学习到的任务相关的特征表示。这通常用于有充足标注数据的场景,比如图像分类任务。 ### 6. PyTorch PyTorch是一个开源机器学习库,由Facebook的人工智能研究团队开发,主要用于计算机视觉和自然语言处理等任务。它被广泛用于研究和生产环境,并且因其易用性、灵活性和动态计算图等特性受到研究人员的青睐。UniMoCo官方实现选择PyTorch作为开发平台,说明了其对科研社区的支持和对易于实现的重视。 ### 7. 可视化表示学习(Visual Representation Learning) 可视化表示学习的目的是从原始视觉数据中提取特征,并将它们转换为能够反映重要信息且更易于处理的形式。在UniMoCo中,无论是无监督、半监督还是全监督学习,最终的目标都是让模型学习到有效的视觉表示,这些表示可以用于下游任务,如图像分类、目标检测、图像分割等。 ### 8. 标签队列(Label Queue) UniMoCo通过标签队列维护受监管的标签,这可能意味着对于那些半监督或全监督学习的任务,模型在进行对比学习时,会参考这些来自标签队列的数据。标签队列机制能帮助模型更好地利用有限的标注数据,增强模型的泛化能力。 ### 结论 UniMoCo的提出,以及其官方PyTorch实现的发布,将对计算机视觉领域产生深远影响。它不仅提供了一个统一的对比学习框架,使得从无监督到全监督的学习过程更加灵活和高效,而且为研究者们提供了一个强力的工具,以便更好地探索和实现各种视觉任务。UniMoCo的研究和应用前景,为机器学习尤其是深度学习在视觉领域的研究和实践提供了新的视角和可能。
recommend-type

【MATLAB算法精讲】:最小二乘法的实现与案例深度分析

# 1. 最小二乘法的基本原理 最小二乘法是一种数学优化技术,它通过最小化误差的平方和来寻找数据的最佳函数匹配。其核心思想是选择一条曲线,使得所有观察点到这条曲线的距离之和最小。这种方法广泛应用于统计学、信号处理、工程学和经济学等领域,尤其适用于需要通过一组数据点来确定函数参数的情况。 ## 1.1 统计学视角下的最小二乘法 在统计学中,最小二乘法经常用于