diff --git a/sql/project-menu.sql b/sql/project-menu.sql index acba6df5..02a6b1a8 100644 --- a/sql/project-menu.sql +++ b/sql/project-menu.sql @@ -26,6 +26,14 @@ INSERT INTO `sys_menu` VALUES(3604, '项目删除', 3600, 4, '#', '', NULL, 1, 0 -- 子菜单:项目导出 INSERT INTO `sys_menu` VALUES(3605, '项目导出', 3600, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:export', '#', 'admin', NOW(), '', NULL, ''); +INSERT IGNORE INTO fastbee.sys_menu_translate (id, zh_cn, en_us) VALUES + (3372, '项目管理', 'Project management'), + (3373, '项目查询', 'Project query'), + (3374, '项目新增', 'Project add'), + (3375, '项目修改', 'Project edit'), + (3376, '项目删除', 'Project remove'), + (3377, '项目导出', 'Project export'); + -- ======================================== -- 菜单说明: -- menu_id: 菜单ID(3600为主菜单,3601-3605为按钮权限) diff --git a/yack-web/src/permission.js b/yack-web/src/permission.js index cb5131ac..9df13bc9 100644 --- a/yack-web/src/permission.js +++ b/yack-web/src/permission.js @@ -30,7 +30,7 @@ router.beforeEach((to, from, next) => { store.dispatch('GenerateRoutes').then((accessRoutes) => { // 根据roles权限生成可访问的路由表 router.addRoutes(accessRoutes); // 动态添加可访问路由表 - next({ path: '/bigscreen', replace: true }); // 登录后跳转到大数据看板 + next({ ...to, replace: true }); // 保持当前目标路由,避免刷新后跳转 }); }) .catch((err) => { diff --git a/yack-web/src/views/bigScreen/home.scss b/yack-web/src/views/bigScreen/home.scss index f48e9e59..8d53342d 100644 --- a/yack-web/src/views/bigScreen/home.scss +++ b/yack-web/src/views/bigScreen/home.scss @@ -1097,7 +1097,22 @@ } .analysis-sidebar { - grid-template-rows: 275px 1fr; + grid-template-rows: 400px 1fr; +} + +// 分析条件卡片:固定高度,不滚动,避免布局跳动 +.analysis-card:first-child { + overflow: hidden; +} + +// 测点列表卡片:内部滚动 +.analysis-card:last-child { + overflow: hidden; + + .model-list { + height: calc(100% - 38px); + overflow: auto; + } } .analysis-content { @@ -1105,6 +1120,10 @@ } .analysis-form { + .el-form-item { + margin-bottom: 12px; + } + .el-form-item__label { color: #a8cff0; } @@ -1228,3 +1247,121 @@ .header-clock__week { font-size: 26px; /* 星期 */ } + +/* ========== 时间范围选择器样式 ========== */ +.time-preset-group { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 12px; + min-height: 64px; // 固定最小高度,容纳2行按钮 +} + +.time-preset-btn { + appearance: none; + -webkit-appearance: none; + outline: none; + height: 28px; + padding: 0 10px; + border: 1px solid rgba(99, 184, 255, .35); + border-radius: 3px; + color: #b8d9ff; + font-size: 12px; + background: rgba(4, 16, 56, .4); + cursor: pointer; + transition: color .18s, background .18s, border-color .18s; + margin: 0; + line-height: 1; + + &:hover { + color: #eaf8ff; + border-color: rgba(104, 224, 255, .6); + background: rgba(12, 45, 110, .55); + } + + &.active { + color: #fff; + border-color: rgba(69, 204, 255, .75); + background: linear-gradient(180deg, rgba(24, 96, 200, .9), rgba(10, 46, 120, .9)); + box-shadow: 0 0 10px rgba(32, 150, 255, .35); + } +} + +.time-range-display { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 8px 10px; + border: 1px solid rgba(71, 190, 255, .3); + border-radius: 4px; + background: rgba(6, 22, 62, .5); + height: 56px; + box-sizing: border-box; +} + +.time-shift-btn { + appearance: none; + -webkit-appearance: none; + outline: none; + width: 32px; + height: 32px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(99, 184, 255, .35); + border-radius: 3px; + color: #b8d9ff; + background: rgba(4, 16, 56, .4); + cursor: pointer; + transition: color .18s, background .18s, border-color .18s; + margin: 0; + + &:hover:not(:disabled) { + color: #eaf8ff; + border-color: rgba(104, 224, 255, .6); + background: rgba(12, 45, 110, .55); + } + + &:disabled { + opacity: .4; + cursor: not-allowed; + } + + i { + font-size: 14px; + font-weight: bold; + } +} + +.time-range-text { + flex: 1; + min-width: 0; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; +} + +.time-range-start, +.time-range-end { + color: #dff7ff; + font-size: 12px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.3; +} + +.time-range-end { + color: #80b5de; +} + +/* 自定义时间选择器样式增强 */ +.analysis-form .el-date-editor--datetimerange { + width: 100%; + margin-top: 8px; +} diff --git a/yack-web/src/views/bigScreen/home.vue b/yack-web/src/views/bigScreen/home.vue index 3c9fc25a..d10d274e 100644 --- a/yack-web/src/views/bigScreen/home.vue +++ b/yack-web/src/views/bigScreen/home.vue @@ -240,13 +240,68 @@ + +
+ + +
+ + +
+ +
+
+ {{ (analysisQuery.timeRange && analysisQuery.timeRange[0]) || '-' }} +
+
+ {{ (analysisQuery.timeRange && analysisQuery.timeRange[1]) || '-' }} +
+
+ +
+ +
查询数据和曲线 @@ -397,13 +452,31 @@ export default { pendingProjectToFocus: null, identifierList: [], analysisLineList: [], + // 预设时间范围配置 + timePresets: [ + { key: '5m', label: '过去 5 分钟', shortLabel: '5分钟', duration: { amount: 5, unit: 'minutes' }, moveStep: 5, moveUnit: 'minutes' }, + { key: '15m', label: '过去 15 分钟', shortLabel: '15分钟', duration: { amount: 15, unit: 'minutes' }, moveStep: 15, moveUnit: 'minutes' }, + { key: '1h', label: '过去 1 小时', shortLabel: '1小时', duration: { amount: 1, unit: 'hours' }, moveStep: 1, moveUnit: 'hours' }, + { key: '4h', label: '过去 4 小时', shortLabel: '4小时', duration: { amount: 4, unit: 'hours' }, moveStep: 4, moveUnit: 'hours' }, + { key: '12h', label: '过去 12 小时', shortLabel: '12小时', duration: { amount: 12, unit: 'hours' }, moveStep: 12, moveUnit: 'hours' }, + { key: '24h', label: '过去 24 小时', shortLabel: '24小时', duration: { amount: 24, unit: 'hours' }, moveStep: 24, moveUnit: 'hours' }, + { key: '2d', label: '过去 2 天', shortLabel: '2天', duration: { amount: 2, unit: 'days' }, moveStep: 2, moveUnit: 'days' }, + { key: '3d', label: '过去 3 天', shortLabel: '3天', duration: { amount: 3, unit: 'days' }, moveStep: 3, moveUnit: 'days' }, + { key: '5d', label: '过去 5 天', shortLabel: '5天', duration: { amount: 5, unit: 'days' }, moveStep: 5, moveUnit: 'days' }, + { key: '7d', label: '过去 7 天', shortLabel: '7天', duration: { amount: 7, unit: 'days' }, moveStep: 7, moveUnit: 'days' }, + ], analysisQuery: { deviceId: null, timeRange: [ - moment().subtract(2, 'hours').format('YYYY-MM-DD HH:mm:ss'), + moment().subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD HH:mm:ss'), - ], + ], // 默认过去1小时,避免初始为空 + activePresetKey: '1h', // 默认选中"过去 1 小时" + isCustomMode: false, // 是否自定义时间模式 }, + customTimeVisible: false, // 自定义时间选择器是否显示 + canShiftLeft: true, // 是否可以向左移动 + canShiftRight: true, // 是否可以向右移动(初始时可以,因为时间窗口可以向前移动) scrollOption: { step: 0.8, limitMoveNum: 4, @@ -602,6 +675,10 @@ export default { mounted() { this.$nextTick(() => this.calcRate()); this.timeFn(); + this.$nextTick(() => { + // 确保 timeRange 已初始化后再更新按钮状态 + this.updateShiftButtonState(); + }); this.loadCockpitData(); this.fetchHeaderUser(); }, @@ -1182,6 +1259,125 @@ export default { this.identifierList = []; }); }, + // ========== 时间范围选择器方法 ========== + // 选择预设时间范围 + selectTimePreset(presetKey) { + const preset = this.timePresets.find(p => p.key === presetKey); + if (!preset) return; + + const endTime = moment(); + const startTime = moment().subtract(preset.duration.amount, preset.duration.unit); + + this.analysisQuery.timeRange = [ + startTime.format('YYYY-MM-DD HH:mm:ss'), + endTime.format('YYYY-MM-DD HH:mm:ss') + ]; + this.analysisQuery.activePresetKey = presetKey; + this.analysisQuery.isCustomMode = false; + this.customTimeVisible = false; + + // 更新移动按钮状态 + this.updateShiftButtonState(); + + // 如果已经选择了设备,自动触发查询 + if (this.analysisQuery.deviceId && this.activeMode === 'analysis') { + this.$nextTick(() => this.runAnalysis()); + } + }, + // 左移时间窗口(往前推) + shiftTimeLeft() { + if (!this.canShiftLeft) return; + + const preset = this.getActivePreset(); + if (!preset) return; + + const [startTime, endTime] = this.analysisQuery.timeRange; + const newStartTime = moment(startTime).subtract(preset.moveStep, preset.moveUnit); + const newEndTime = moment(endTime).subtract(preset.moveStep, preset.moveUnit); + + // 更新时间范围 + this.analysisQuery.timeRange = [ + newStartTime.format('YYYY-MM-DD HH:mm:ss'), + newEndTime.format('YYYY-MM-DD HH:mm:ss') + ]; + + // 更新移动按钮状态 + this.updateShiftButtonState(); + + // 自动触发查询 + if (this.analysisQuery.deviceId) { + this.$nextTick(() => this.runAnalysis()); + } + }, + // 右移时间窗口(往后推) + shiftTimeRight() { + if (!this.canShiftRight) return; + + const preset = this.getActivePreset(); + if (!preset) return; + + const [startTime, endTime] = this.analysisQuery.timeRange; + const newStartTime = moment(startTime).add(preset.moveStep, preset.moveUnit); + const newEndTime = moment(endTime).add(preset.moveStep, preset.moveUnit); + + // 不允许超过当前时间 + if (newEndTime.isAfter(moment())) return; + + // 更新时间范围 + this.analysisQuery.timeRange = [ + newStartTime.format('YYYY-MM-DD HH:mm:ss'), + newEndTime.format('YYYY-MM-DD HH:mm:ss') + ]; + + // 更新移动按钮状态 + this.updateShiftButtonState(); + + // 自动触发查询 + if (this.analysisQuery.deviceId) { + this.$nextTick(() => this.runAnalysis()); + } + }, + // 启用自定义时间模式 + enableCustomMode() { + this.analysisQuery.isCustomMode = true; + this.analysisQuery.activePresetKey = null; + this.canShiftLeft = false; + this.canShiftRight = false; + // 强制触发视图更新 + this.$forceUpdate(); + }, + // 自定义时间改变事件 + handleCustomTimeChange() { + // 自定义时间模式下,禁用移动按钮 + this.canShiftLeft = false; + this.canShiftRight = false; + }, + // 获取当前激活的预设配置 + getActivePreset() { + if (!this.analysisQuery.activePresetKey) return null; + return this.timePresets.find(p => p.key === this.analysisQuery.activePresetKey); + }, + // 更新移动按钮状态 + updateShiftButtonState() { + const preset = this.getActivePreset(); + + if (!preset || this.analysisQuery.isCustomMode) { + // 自定义时间模式下,禁用移动按钮 + this.canShiftLeft = false; + this.canShiftRight = false; + return; + } + + const [startTime, endTime] = this.analysisQuery.timeRange; + const now = moment(); + + // 检查是否可以向左移动(总是可以,除非已经是历史最早时间) + this.canShiftLeft = true; + + // 检查是否可以向右移动(结束时间 + 步长 不能超过当前时间) + const testEndTime = moment(endTime).add(preset.moveStep, preset.moveUnit); + this.canShiftRight = testEndTime.isSameOrBefore(now); + }, runAnalysis() { const device = this.selectedAnalysisDevice; if (!device) {