Bladeren bron

Merge remote-tracking branch 'origin/master'

null 10 uur geleden
bovenliggende
commit
e572d06cfe
2 gewijzigde bestanden met toevoegingen van 38 en 71 verwijderingen
  1. 0 66
      memory/sessions/2026-10-21.md
  2. 38 5
      src/views/portal/index.vue

+ 0 - 66
memory/sessions/2026-10-21.md

@@ -1,66 +0,0 @@
-# 2026-10-21:删除事件 department 字段
-
-## 目标与结论
-
-- 按用户要求移除 `dis_event.department` 及前后端对应字段引用。
-- PC 删除转接报送及综合查询的责任单位筛选、综合查询/催办的责任单位列、公共事件详情及催办详情展示、催办接收部门的旧字段回退;核实表单不再回显或提交单位名称。
-- 最初保留核实 `deptId` 作为处置人筛选条件;该决定已被下文“核实操作移除处置单位”的用户要求取代。
-- 后端删除实体、核实/分页 DTO、事件/催办 VO 的字段及写入/查询/映射逻辑;移动端 Controller 删除按旧名称反查部门 ID 的兼容逻辑。
-- 移动端 `src/services/emergency.ts` 与 `src/pages/emergency/event-detail.vue` 同步移除类型字段、提交参数和详情展示。
-
-## 文件变更
-
-- PC:`disOnDuty/cphs.vue`、`zjbs.vue`、`gzcb.vue`、`EventDetailDrawer.vue`、`disAssess/DisInquiry.vue`。
-- 后端:`DisEvent`、`DisEventPageDTO`、`DisEventVO`、`EventUrgeRowVO`、`EventVerifyDTO`、`DisEventServiceImpl`、`MobileEmergencyController`,以及部门选择器相关说明。
-- 主结构脚本 `pipe-network-service/sql/dispatch_refactor.sql` 移除建列/补列/注释并加入幂等删列;新增独立升级脚本 `pipe-network-service/sql/dispatch_drop_event_department.sql`。
-
-## 验证与风险
-
-- 后端 `mvn -pl pipe-network-service/zksy-admin -am -DskipTests compile -q` 通过。
-- PC `npm run build:prod` 通过,构建日志位于工作区 `department-frontend-build.log`。
-- 移动端 `npm run type-check` 通过,三个仓库 `git diff --check` 通过。
-- 应急事件运行时代码检索已无 `department` 字段引用;SQL 中仅保留删列和删列核验引用。
-- 尚未连接目标 KingbaseES 执行脚本或验证真实登录态页面;部署新版前后端后需执行独立删列脚本。既有其他未提交修改和已暂存文档删除均保留。
-
-## 事件状态编号调整
-
-- 目标:按用户要求将 `EventStatusEnum` 调整为 3 待验收、4 已解除、5 不实事件,0/1/2 保持原意。
-- 后端调整枚举顺序与编码、实体/Service/DTO 状态说明;业务流转、统计和日志名称沿用枚举引用。
-- PC 同步核实页不实事件筛选、跟踪页状态范围及核验按钮、总结评估已解除筛选,以及所有事件标签映射;API 流程注释同步六态。
-- 移动端同步事件状态名称/枚举名/颜色映射、列表标签筛选值和详情核验入口。
-- 新增 `pipe-network-service/sql/migration_dispatch_event_status.sql`:旧值 3→4、4→5、5→3;同一事务更新 `dis_event.status` 及 `dis_event_log.status_from/status_to`,加锁并记录迁移标记以防重复轮换。
-- 主结构脚本同步六态说明;历史中文迁移继续生成旧数字编码,明确必须在其完成后执行状态升级脚本。空库新建不执行状态升级脚本,已升级库不得重跑历史迁移。
-- 验证:后端编译通过;`EventStatusContractTest` 3 项通过(待验收可核验且未办结、终态不可核验、列表编码与活动状态范围);PC `npm run build:prod`、移动端 `npm run type-check` 通过;三个仓库 `git diff --check` 通过。
-- 风险与后续:数据库脚本尚未实际执行;存量环境须停止旧应用写入后执行状态升级脚本,再同步部署前后端。未验证真实登录态页面;保留所有既有未提交变更和文档删除。
-
-## 核实操作移除处置单位
-
-- 目标与结论:用户明确要求 PC `cphs.vue` 和移动端 `event-detail.vue` 的核实操作也删除“处置单位”;纠正此前将其保留为处置人筛选条件的理解。
-- 两端删除处置单位控件、部门请求、部门树/下级部门筛选、必填校验、回显和 `deptId` 提交参数;处置人直接从有效系统用户中选择,用户候选标签保留所属部门名称以区分人员。
-- 移动端 `src/services/emergency.ts` 删除无其他调用的部门选项类型与请求;后端 `EventVerifyDTO` 删除 `deptId`,`DisEventServiceImpl` 删除部门必填和处置人所属部门校验,保留有效用户校验,并同步相关说明。
-- 验证:PC `npm run build:prod`、移动端 `npm run type-check`、后端管理模块依赖编译均通过;`EventVerifyWithoutDepartmentTest` 2 项通过,覆盖无所属部门的有效处置人可核实,以及无效处置人仍被拒绝;三个仓库 `git diff --check` 通过,两个核实页面已无处置单位/`deptId` 引用。
-- 风险与后续:本次修正无需新增数据库迁移;需同步部署 PC、移动端和后端后验证真实登录态操作。保留既有未提交变更及已暂存文档删除。
-
-## 核实选人按权限过滤
-
-- 用户确认处置人、核验人必须具备后续操作权限;候选接口拆分为 `/api/dispatch/event/handlerOptions`(`dispatch:event:handle`)和 `/api/dispatch/event/verifierOptions`(`dispatch:event:audit`),仅返回启用且未删除的用户。
-- PC、移动端核实表单分别使用两组候选列表;候选接口要求当前登录用户具备事件核实权限。
-- 后端 `DisEventServiceImpl` 在核实提交时再次检查处置人/核验人权限,管理员按系统管理员规则放行;无对应权限即拒绝,避免绕过前端提交无权限人员。
-- 验证:后端管理模块编译、5 项 `EventVerifyWithoutDepartmentTest`、PC 生产构建、移动端类型检查均通过。无需数据库迁移;需同步部署后端与两端前端。
-
-## 事件日志多附件地址统一处理
-
-- 结论:附件访问地址由后端统一转换,数据库继续保存相对路径,接口展示出口返回完整可访问 URL;PC/移动端只负责按逗号拆分并渲染,不再各自拼接 MinIO 前缀。
-- 修复 `DispatchAttachUrlSupport`:`toAccessibleUrl` 和 `toRelative` 均按逗号逐项处理,支持 `logs.attachUrl` 多图、混合历史完整 URL 与相对路径,避免把整串多图地址拼成一个错误 URL。
-- `api/mobile/emergency/events/{id}` 通过事件日志服务返回的 `logs.attachUrl` 将得到 `url1,url2,...`,每一项均可直接用于图片预览。
-- 验证:`DispatchAttachUrlSupportTest` 2 项通过(多图完整地址转换、混合地址相对化),后端管理模块编译通过,`git diff --check` 通过;无需数据库迁移。
-
-## 设备台账设备类型两级快捷筛选
-
-- 目标:按 `prd/设备台账-设备类型快捷筛选.html` 将设备台账的设备类型查询下拉替换为两级平铺选择。
-- `equipmentBase/index.vue` 新增一级大类按钮和二级浅底胶囊;默认一级全部、二级隐藏,选中大类才展开对应子类型。切换大类清空旧二级,点击立即查询并回到第一页;重置清空两级选择及其他查询条件。
-- 使用 `/equipmentType` 返回的真实分类,不采用原型示例分类与模拟数量;新增/编辑表单的类型树下拉保持原样。
-- 复用后端已有 `equipmentTypeIds` 集合参数过滤大类,`equipmentTypeId` 精确过滤二级;保留菜单 `rootTypeId` 范围,未分类按实际叶子 ID 查询,空大类使用无效 ID 避免退化为全部数据,不改后端或数据库。
-- 新增 `tests/equipmentTypeQuickFilter.test.mjs` 并接入 `npm test`,5 项回归覆盖默认、两级联动、后端分页参数、切换清空、重置、菜单范围、未分类与空大类。
-- 验证:新增 5 项及既有 6 项测试通过,生产构建通过(有既有 Sass 弃用与打包体积提示),`git diff --check` 通过;未进行真实登录态浏览器联调。
-- 后续:部署前端后确认真实设备分类、筛选结果与分页;无需新增接口或数据库迁移。

+ 38 - 5
src/views/portal/index.vue

@@ -38,7 +38,7 @@
           <div class="stat-value">{{ formatCount(statistics.deviceTotal) }}</div>
           <div class="stat-sub">
             <span>离线 <b class="text-primary">{{ formatCount(statistics.offlineDeviceCount) }}</b></span>
-            <span>预警 <b class="text-orange">{{ formatCount(statistics.alarmingDeviceCount) }}</b></span>
+            <span>报警 <b class="text-orange">{{ formatCount(statistics.alarmingDeviceCount) }}</b></span>
           </div>
         </article>
         <article class="stat-card">
@@ -112,6 +112,7 @@ const currentDate = ref('')
 const noticeScrollRef = ref()
 const noticeTrackRef = ref()
 const noticePaused = ref(false)
+const shouldScrollNotices = ref(false)
 const noticeDialogVisible = ref(false)
 const noticeDetailLoading = ref(false)
 const noticeDetail = ref({})
@@ -122,7 +123,7 @@ let lastNoticeFrame = 0
 let noticeRequestId = 0
 
 const displayName = computed(() => userStore.nickName || userStore.name || '用户')
-const scrollingNotices = computed(() => notices.value.length ? notices.value.concat(notices.value) : [])
+const scrollingNotices = computed(() => shouldScrollNotices.value ? notices.value.concat(notices.value) : notices.value)
 const warningTrend = computed(() => buildTrend(statistics.value.todayWarningCount, statistics.value.yesterdayWarningCount))
 const alarmTrend = computed(() => buildTrend(statistics.value.todayAlarmCount, statistics.value.yesterdayAlarmCount))
 const modules = computed(() => {
@@ -221,7 +222,7 @@ function noticeDotClass(index) {
 }
 
 function animateNotices(timestamp) {
-  if (!noticePaused.value && notices.value.length && timestamp - lastNoticeFrame >= 30) {
+  if (!noticePaused.value && shouldScrollNotices.value && timestamp - lastNoticeFrame >= 30) {
     const scroll = noticeScrollRef.value
     const track = noticeTrackRef.value
     if (scroll && track) {
@@ -241,17 +242,47 @@ function animateNotices(timestamp) {
   noticeAnimationFrame = requestAnimationFrame(animateNotices)
 }
 
+async function updateNoticeScrollState() {
+  await nextTick()
+  const scroll = noticeScrollRef.value
+  const track = noticeTrackRef.value
+  if (!scroll || !track || !notices.value.length) {
+    shouldScrollNotices.value = false
+    noticeOffset = 0
+    return
+  }
+
+  const duplicateStart = track.querySelector('[data-copy-start="true"]')
+  const firstNotice = track.firstElementChild
+  const lastOriginalNotice = track.children[notices.value.length - 1]
+  const contentWidth = duplicateStart && firstNotice
+    ? lastOriginalNotice.getBoundingClientRect().right - firstNotice.getBoundingClientRect().left
+    : track.scrollWidth
+  const needsScroll = contentWidth > scroll.clientWidth + 1
+  shouldScrollNotices.value = needsScroll
+  if (!needsScroll) {
+    noticeOffset = 0
+    scroll.scrollLeft = 0
+  } else {
+    await nextTick()
+    if (noticeScrollRef.value) noticeScrollRef.value.scrollLeft = Math.floor(noticeOffset)
+  }
+}
+
 async function loadPortalData() {
   const [statisticsResult, noticesResult] = await Promise.allSettled([getPortalStatistics(), getPortalNotices()])
   if (statisticsResult.status === 'fulfilled' && statisticsResult.value.data) statistics.value = statisticsResult.value.data
   if (noticesResult.status === 'fulfilled') {
     notices.value = Array.isArray(noticesResult.value.data) ? noticesResult.value.data : []
-    await nextTick()
     noticeOffset = 0
-    if (noticeScrollRef.value) noticeScrollRef.value.scrollLeft = 0
+    await updateNoticeScrollState()
   }
 }
 
+function handleNoticeResize() {
+  updateNoticeScrollState()
+}
+
 async function openNotice(noticeId) {
   const requestId = ++noticeRequestId
   noticeDialogVisible.value = true
@@ -289,12 +320,14 @@ function logout() {
 onMounted(() => {
   updateCurrentDate()
   dateTimer = window.setInterval(updateCurrentDate, 60000)
+  window.addEventListener('resize', handleNoticeResize)
   noticeAnimationFrame = requestAnimationFrame(animateNotices)
   loadPortalData()
 })
 
 onUnmounted(() => {
   window.clearInterval(dateTimer)
+  window.removeEventListener('resize', handleNoticeResize)
   cancelAnimationFrame(noticeAnimationFrame)
 })
 </script>