From 335dc88a2a7c5b55fbf46fa613023e0d5fa5de7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=96=87=E6=98=8A?= Date: Wed, 22 Apr 2026 16:32:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=BB=BC=E5=90=88=E8=BF=90=E8=90=A5?= =?UTF-8?q?=EF=BC=9A=E5=88=86=E6=94=AF=E5=9B=BEBUG=E3=80=81=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=20feat:=E5=AE=8C=E5=96=84=E8=BF=9B=E5=BA=A6/=E6=80=BB?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/oa/project/dashboard2/index.vue | 142 +++++++++++++++--- .../oa/project/pace/components/xmind.vue | 89 +++++++++-- 2 files changed, 198 insertions(+), 33 deletions(-) diff --git a/ruoyi-ui/src/views/oa/project/dashboard2/index.vue b/ruoyi-ui/src/views/oa/project/dashboard2/index.vue index 9797b8b..04a02e2 100644 --- a/ruoyi-ui/src/views/oa/project/dashboard2/index.vue +++ b/ruoyi-ui/src/views/oa/project/dashboard2/index.vue @@ -158,9 +158,47 @@ - +
- +
+ +
+ + +
+ 项目名: + + + + +
+
+ +
+ 项目负责人: + {{ projectManagerName }} +
+
+ +
+ 当前进度: + {{ dashboardScheduleSummary }} +
+
+ +
+ 项目状态: + 重点关注 + 一般项目 +
+
+
+
+
+
+
@@ -206,6 +244,7 @@
import { mapState } from 'vuex' import Xmind from '@/views/oa/project/pace/components/xmind.vue' +import ProjectInfo from '@/components/fad-service/ProjectInfo/index.vue' export default { name: 'OaProjectDashboard2', - components: { Xmind }, + components: { Xmind, ProjectInfo }, dicts: ['sys_work_type', 'sys_sort_grade'], data () { return { @@ -265,8 +305,31 @@ export default { 'projectTotal', 'taskQuery', 'taskList', - 'stepList' + 'stepList', + 'projectDetail' ]), + projectDisplayName () { + const p = this.projectDetail + return (p && p.projectName) ? String(p.projectName) : '' + }, + projectManagerName () { + const p = this.projectDetail + return (p && p.functionary) ? String(p.functionary) : '' + }, + projectIsTop () { + const p = this.projectDetail + if (!p) return false + const top = p.isTop + return top === true || top === 1 || top === '1' + }, + /** 与 pace/step 中 scheduleSummary 文案规则一致 */ + dashboardScheduleSummary () { + const list = this.stepList || [] + const totalCount = list.length + const completedCount = list.filter((item) => item.status === 2).length + const pendingCount = list.filter((item) => item.status === 1).length + return `已完成(${completedCount})+ 待验收(${pendingCount}) / 总节点数(${totalCount})` + }, filteredTaskList () { const list = this.taskList || [] const code = (this.taskQuery.projectCode || '').trim().toLowerCase() @@ -304,6 +367,19 @@ export default { this.getProjectList() }, methods: { + /** 进度导图挂在 Tab 内,切换为可见后触发 ECharts 重新测量,避免线上画布宽/高为 0 */ + onScheduleTabClick (tab) { + if (!tab || tab.name !== 'mind') return; + this.$nextTick(() => { + requestAnimationFrame(() => { + const comp = this.$refs.dashboardMindXmind; + if (comp && typeof comp.scheduleResize === 'function') { + comp.scheduleResize(); + } + }); + }); + }, + async getProjectList () { this.projectLoading = true try { @@ -360,6 +436,16 @@ export default { } finally { this.pageLoading = false this.xmindLoading = false + if (this.scheduleViewTab === 'mind') { + this.$nextTick(() => { + requestAnimationFrame(() => { + const comp = this.$refs.dashboardMindXmind + if (comp && typeof comp.scheduleResize === 'function') { + comp.scheduleResize() + } + }) + }) + } } }, @@ -666,16 +752,18 @@ export default { padding: 0; } -.schedule-panel-tabs { - flex: 1 1 0%; - min-height: 0; - display: flex; - flex-direction: column; - +.schedule-board-summary--pace-like { + flex-shrink: 0; + padding: 10px 12px 8px; + border-bottom: 1px solid #ebeef5; } -/* Element Tabs 根节点参与纵向 flex,才能把剩余高度交给内容区 */ -.schedule-panel-tabs :deep(.el-tabs) { +/* + * schedule-panel-tabs 与 .el-tabs 在同一 DOM 节点上,不能用「.schedule-panel-tabs :deep(.el-tabs)」后代选择器(永远匹配不到)。 + * 生产包合并 CSS 后,Element 全局 .el-tabs 可能后加载并盖住单独的 .schedule-panel-tabs,导致 flex 丢失。 + * 使用双类选择器提高命中与优先级。 + */ +.schedule-panel-tabs.el-tabs { flex: 1 1 0%; min-height: 0; display: flex; @@ -683,13 +771,13 @@ export default { height: 100%; } -.schedule-panel-tabs :deep(.el-tabs__header) { +.schedule-panel-tabs.el-tabs :deep(.el-tabs__header) { margin-bottom: 0; padding: 0 12px; flex-shrink: 0; } -.schedule-panel-tabs :deep(.el-tabs__content) { +.schedule-panel-tabs.el-tabs :deep(.el-tabs__content) { flex: 1 1 0%; min-height: 0; overflow: hidden; @@ -699,9 +787,11 @@ export default { } /* 当前激活的 pane 占满内容区高度,子元素才能算出可滚动区域 */ -.schedule-panel-tabs :deep(.el-tab-pane) { +.schedule-panel-tabs.el-tabs :deep(.el-tab-pane) { flex: 1 1 0%; min-height: 0; + min-width: 0; + width: 100%; overflow: hidden; display: flex; flex-direction: column; @@ -710,6 +800,8 @@ export default { .schedule-tab-pane-inner { flex: 1 1 0%; min-height: 0; + min-width: 0; + width: 100%; height: 100%; box-sizing: border-box; display: flex; @@ -719,19 +811,24 @@ export default { .schedule-tab-pane-inner--mind { min-height: 280px; overflow: hidden; + width: 100%; + min-width: 0; } -/* 进度明细 Tab:占满下方板块;内部仅滚动区参与滚动 */ +/* 进度明细 Tab:表格与面板四边留白,避免贴边 */ .progress-table-pane { flex: 1 1 0%; min-height: 0; width: 100%; - height:300px; + height: 300px; box-sizing: border-box; display: flex; flex-direction: column; - padding: 8px 12px 10px; - + padding: 14px 18px 18px; +} + +.progress-table-pane :deep(.el-empty) { + padding: 24px 8px; } .progress-table-scroll { @@ -740,6 +837,8 @@ export default { min-width: 0; overflow: auto; -webkit-overflow-scrolling: touch; + /* 表格与滚动容器边缘再留一线间距,滚动条不压表格边框 */ + padding: 2px 4px 4px 2px; } .progress-step-el-table { @@ -751,7 +850,10 @@ export default { flex-direction: row; align-items: stretch; flex: 1; + flex-basis: 0; min-height: 0; + min-width: 0; + width: 100%; } .task-table-wrap { @@ -804,7 +906,7 @@ export default { } .xmind-wrap { - flex: 1; + flex: 1 1 0%; min-width: 0; position: relative; padding: 0; diff --git a/ruoyi-ui/src/views/oa/project/pace/components/xmind.vue b/ruoyi-ui/src/views/oa/project/pace/components/xmind.vue index f636f2d..099056f 100644 --- a/ruoyi-ui/src/views/oa/project/pace/components/xmind.vue +++ b/ruoyi-ui/src/views/oa/project/pace/components/xmind.vue @@ -106,7 +106,9 @@ export default { return { width: '100%', height: this.height, - minHeight: this.dashboardMode ? '300px' : '240px' + minHeight: this.dashboardMode ? '300px' : '240px', + /* 看板嵌在横向 flex 内,避免 min-width:auto 把可用宽压成 0 */ + ...(this.dashboardMode ? { minWidth: 0, boxSizing: 'border-box' } : {}) } } }, @@ -118,37 +120,92 @@ export default { clickEvent: null, // 新增:存储点击事件句柄,用于销毁解绑 users: [], supplierList: [], + chartResizeObserver: null, + chartResizeObserverRaf: null }; }, watch: { - // 监听列表数据变化,自动更新图表 list: { deep: true, handler () { - if (this.chartInstance) { + this.$nextTick(() => { this.initChart(); - } + }); } } }, mounted () { this.initChart(); - this.$nextTick(() => this.resizeChart()); + this.deferResizeChart(); window.addEventListener('resize', this.resizeChart); + this.$nextTick(() => { + this.bindChartResizeObserver(); + }); }, beforeDestroy () { window.removeEventListener('resize', this.resizeChart); - // 新增:解绑Echarts点击事件,防止内存泄漏 + this.unbindChartResizeObserver(); if (this.chartInstance && this.clickEvent) { this.chartInstance.off('click', this.clickEvent); } - // 销毁图表实例,防止内存泄漏 this.chartInstance?.dispose(); }, methods: { - // 优化:增加防抖处理-窗口自适应,避免频繁触发 resizeChart () { - this.chartInstance?.resize() + this.chartInstance?.resize(); + }, + + /** + * 供综合看板在切换 Tab 后调用:Tab 从 display:none 变为可见后需重新测量画布。 + */ + scheduleResize () { + this.deferResizeChart(); + }, + + /** + * el-tab / flex 布局下首帧常为 0 宽高,生产环境更明显;延迟到布局稳定后再 resize。 + */ + deferResizeChart () { + this.$nextTick(() => { + this.$nextTick(() => { + requestAnimationFrame(() => { + this.resizeChart(); + requestAnimationFrame(() => this.resizeChart()); + }); + }); + }); + }, + + bindChartResizeObserver () { + if (!this.dashboardMode || typeof ResizeObserver === 'undefined') { + return; + } + const el = this.$refs.chart; + if (!el) { + return; + } + this.unbindChartResizeObserver(); + this.chartResizeObserver = new ResizeObserver(() => { + if (this.chartResizeObserverRaf != null) { + cancelAnimationFrame(this.chartResizeObserverRaf); + } + this.chartResizeObserverRaf = requestAnimationFrame(() => { + this.chartResizeObserverRaf = null; + this.resizeChart(); + }); + }); + this.chartResizeObserver.observe(el); + }, + + unbindChartResizeObserver () { + if (this.chartResizeObserverRaf != null) { + cancelAnimationFrame(this.chartResizeObserverRaf); + this.chartResizeObserverRaf = null; + } + if (this.chartResizeObserver) { + this.chartResizeObserver.disconnect(); + this.chartResizeObserver = null; + } }, handleSubmit () { @@ -288,11 +345,13 @@ export default { }; }, - // 初始化图表 initChart () { - // 初始化图表实例 + const dom = this.$refs.chart; + if (!dom) { + return; + } if (!this.chartInstance) { - this.chartInstance = echarts.init(this.$refs.chart); + this.chartInstance = echarts.init(dom); } // 重要:先解绑已有点击事件,防止多次绑定导致弹窗多次触发 if (this.clickEvent) { @@ -422,8 +481,8 @@ export default { } ] }; - // 渲染图表 this.chartInstance?.setOption(option, true); + this.deferResizeChart(); this.clickEvent = (params) => { const data = params.data; @@ -461,12 +520,16 @@ export default { flex-direction: column; height: 100%; min-height: 0; + width: 100%; + min-width: 0; } /* 与综合看板折线图区域一致:白底、细边框、轻圆角 */ .xmind-box--dashboard .xmind-container { flex: 1; min-height: 0; + min-width: 0; + width: 100%; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; From e0e31c765b3fb528a02c52d007ab9cc42dc2e040 Mon Sep 17 00:00:00 2001 From: zuqijia <2924963185@qq.com> Date: Wed, 22 Apr 2026 18:00:41 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=8A=84?= =?UTF-8?q?=E9=80=81=E6=A0=87=E8=AE=B0=E6=9C=AA=E8=AF=BB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hrm/controller/HrmFlowCcController.java | 9 ++++++++- .../ruoyi/hrm/service/IHrmFlowCcService.java | 4 ++++ .../hrm/service/impl/HrmFlowCcServiceImpl.java | 17 +++++++++++++++++ ruoyi-ui/src/api/hrm/cc.js | 8 +++++++- ruoyi-ui/src/views/hrm/flow/cc.vue | 10 +++++++++- 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmFlowCcController.java b/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmFlowCcController.java index ea434d7..ff5dcf1 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmFlowCcController.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmFlowCcController.java @@ -66,7 +66,14 @@ public class HrmFlowCcController extends BaseController { Long userId = LoginHelper.getUserId(); return toAjax(service.markRead(ccId, userId)); } - + /** + * 标记抄送未读(新增) + */ + @PostMapping("/{ccId}/unread") + public R unread(@PathVariable Long ccId) { + Long userId = LoginHelper.getUserId(); + return toAjax(service.markUnread(ccId, userId)); + } @GetMapping("/ping") public R ping(@RequestParam @NotNull String x) { return R.ok(x); diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/service/IHrmFlowCcService.java b/fad-hrm/src/main/java/com/ruoyi/hrm/service/IHrmFlowCcService.java index 9dab1ca..a94c427 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/service/IHrmFlowCcService.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/service/IHrmFlowCcService.java @@ -27,5 +27,9 @@ public interface IHrmFlowCcService { * 标记已读 */ Boolean markRead(Long ccId, Long userId); + /** + * 标记未读 + */ + Boolean markUnread(Long ccId, Long userId); } diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowCcServiceImpl.java b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowCcServiceImpl.java index 2d4a292..28db56c 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowCcServiceImpl.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowCcServiceImpl.java @@ -99,6 +99,23 @@ public class HrmFlowCcServiceImpl implements IHrmFlowCcService { .eq(HrmFlowCc::getDelFlag, 0) ) > 0; } + /** + * 标记未读 + */ + @Override + public Boolean markUnread(Long ccId, Long userId) { + if (ccId == null || userId == null) { + return false; + } + return baseMapper.update( + null, + Wrappers.lambdaUpdate() + .set(HrmFlowCc::getReadFlag, 0) + .eq(HrmFlowCc::getCcId, ccId) + .eq(HrmFlowCc::getCcUserId, userId) + .eq(HrmFlowCc::getDelFlag, 0) + ) > 0; + } private LambdaQueryWrapper buildQueryWrapper(HrmFlowCcBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); diff --git a/ruoyi-ui/src/api/hrm/cc.js b/ruoyi-ui/src/api/hrm/cc.js index 1bf5c74..a6b8998 100644 --- a/ruoyi-ui/src/api/hrm/cc.js +++ b/ruoyi-ui/src/api/hrm/cc.js @@ -16,7 +16,13 @@ export function readCc(ccId) { method: 'post' }) } - +// 标记抄送为未读 +export function unreadCc(ccId) { + return request({ + url: `/hrm/flow/cc/${ccId}/unread`, + method: 'post' + }) +} // 手动抄送 export function addCc(data) { return request({ diff --git a/ruoyi-ui/src/views/hrm/flow/cc.vue b/ruoyi-ui/src/views/hrm/flow/cc.vue index 49bc06b..a2ca8d4 100644 --- a/ruoyi-ui/src/views/hrm/flow/cc.vue +++ b/ruoyi-ui/src/views/hrm/flow/cc.vue @@ -56,8 +56,8 @@