系统设置:设备管理查下级机构

This commit is contained in:
zhugongping 2026-10-05 18:26:40 +08:00
parent fe8d204d11
commit 7fc8b14781
11 changed files with 245 additions and 111 deletions

View File

@ -1,12 +1,10 @@
package com.fastbee.web.controller.system;
import com.fastbee.common.annotation.Log;
import com.fastbee.common.constant.UserSettingConstants;
import com.fastbee.common.core.controller.BaseController;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.enums.BusinessType;
import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.system.domain.LoginRedirectOption;
import com.fastbee.system.domain.SysUserSetting;
import com.fastbee.system.service.ISysUserSettingService;
@ -15,8 +13,8 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -80,25 +78,30 @@ public class SysUserSettingController extends BaseController {
return AjaxResult.success();
}
@ApiOperation("保存当前用户的设置")
/**
* 部分更新:只提交、也只写「用户改动过的那几项」
*
* <p>刻意不是整体覆盖写(PUT):设置页有多个互相独立的配置项,
* 整体覆盖会让「保存 A 配置」把 B 配置冲回默认值。
* 这里按入参中非 null 的字段逐列更新,未传的字段在 SQL 层面就碰不到。
* <p>用 PATCH 而非 PUT:PUT 的语义是整体替换,与部分更新名实不符。
*/
@ApiOperation("部分更新当前用户的设置")
@Log(title = "个人设置", businessType = BusinessType.UPDATE)
@PutMapping()
public AjaxResult save(@RequestBody SysUserSetting setting) {
Long userId = SecurityUtils.getUserId();
String redirect = setting.getLoginRedirect();
if (StringUtils.isEmpty(redirect)) {
// 空值不覆盖,交由默认值兜底,避免把用户显式配置冲掉
redirect = UserSettingConstants.DEFAULT_LOGIN_REDIRECT;
@PatchMapping()
public AjaxResult update(@RequestBody SysUserSetting setting) {
if (setting.getLoginRedirect() == null
&& setting.getQueryGatewayChild() == null
&& setting.getShowChild() == null) {
return AjaxResult.error("没有需要保存的配置项");
}
if (!isValidLoginRedirect(redirect)) {
// 只有在本次确实要改「登录后显示页面」时才做白名单校验;
// 未传(null)表示本次不改它,不能顺手用默认值把它覆盖掉
if (setting.getLoginRedirect() != null && !isValidLoginRedirect(setting.getLoginRedirect())) {
return AjaxResult.error("登录后跳转页面不合法,请从系统菜单中选择");
}
setting.setLoginRedirect(redirect);
// 审计字段由服务端填充,不接受前端传入,避免越权写入他人操作名
String username = SecurityUtils.getUsername();
setting.setCreateBy(username);
setting.setUpdateBy(username);
userSettingService.save(userId, setting);
userSettingService.updatePartial(SecurityUtils.getUserId(), setting, SecurityUtils.getUsername());
return AjaxResult.success();
}

View File

@ -69,6 +69,17 @@ public class UserSettingConstants {
*/
public static final boolean DEFAULT_QUERY_GATEWAY_CHILD = false;
/**
* 设备管理列表是否包含下级机构数据的默认值
*
* <p>语义对应 {@code Device.showChild}:仅在机构用户({@code deptId} 非空)时参与 SQL,
* 开启时 {@code tenant_id} 落在「本机构及全部下级机构用户」集合内,关闭时仅本机构。
* 原实现是设备管理页的勾选框 + 浏览器 localStorage(key {@code device-showChild}),
* 换浏览器/换电脑就丢,现改为落库、由「系统管理 → 系统设置」页维护。
* <p>默认 false = 与引入本开关前完全一致(默认列表不含下级机构数据)。
*/
public static final boolean DEFAULT_SHOW_CHILD = false;
private UserSettingConstants() {
}

View File

@ -60,6 +60,17 @@ public class SysUserSetting extends BaseEntity {
@ApiModelProperty(value = "是否查询网关子设备(按编号查时换算网关并放开子设备类产品过滤)")
private Boolean queryGatewayChild;
/**
* 设备管理列表是否包含下级机构数据
*
* <p>对应 {@code Device.showChild}:仅在机构用户(deptId 非空)时参与 SQL,
* 开启时 {@code tenant_id} 落在「本机构及全部下级机构用户」集合内,关闭时仅本机构。
* 原为设备管理页勾选框 + 浏览器 localStorage,现改为落库、由系统设置页维护;
* 设备管理页只读取该值作为查询参数下发,页面内不再可就地修改。
*/
@ApiModelProperty(value = "设备管理列表是否包含下级机构数据")
private Boolean showChild;
public SysUserSetting() {
}
@ -75,6 +86,7 @@ public class SysUserSetting extends BaseEntity {
setting.setLoginRedirect(UserSettingConstants.DEFAULT_LOGIN_REDIRECT);
setting.setPinnedTags(UserSettingConstants.EMPTY_PINNED_TAGS);
setting.setQueryGatewayChild(UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD);
setting.setShowChild(UserSettingConstants.DEFAULT_SHOW_CHILD);
return setting;
}
@ -118,6 +130,14 @@ public class SysUserSetting extends BaseEntity {
this.queryGatewayChild = queryGatewayChild;
}
public Boolean getShowChild() {
return showChild;
}
public void setShowChild(Boolean showChild) {
this.showChild = showChild;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
@ -126,6 +146,7 @@ public class SysUserSetting extends BaseEntity {
.append("loginRedirect", getLoginRedirect())
.append("pinnedTags", getPinnedTags())
.append("queryGatewayChild", getQueryGatewayChild())
.append("showChild", getShowChild())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())

View File

@ -23,12 +23,16 @@ public interface SysUserSettingMapper {
SysUserSetting selectByUserId(Long userId);
/**
* 保存设置(存在则更新,不存在则插入)
* 部分更新设置:只更新入参中**非 null** 的列,未传的列保持库中原值
*
* @param setting 设置
* <p>调用方先 {@link #insertDefaultIfAbsent} 保证行存在,否则 UPDATE 0 行会静默丢改动。
* 刻意不提供「整体覆盖写」的入口:设置页每次只提交用户改动过的那几项,
* 整体覆盖会让「保存 A 配置」把 B 配置冲回默认值。
*
* @param setting 设置,userId 必填,其余字段只填本次要改的
* @return 影响行数
*/
int upsert(SysUserSetting setting);
int updateSetting(SysUserSetting setting);
/**
* 仅更新固定标签列(整列覆盖)
@ -46,12 +50,13 @@ public interface SysUserSettingMapper {
* 确保用户设置行存在:不存在则插入默认行,已存在则不做任何修改
*
* <p>固定标签的增量写入要在一行上做「锁定读 → 改 → 写」,行必须先存在。
* 刻意不用 {@link #upsert}:upsert 的 ON DUPLICATE 子句会覆盖
* login_redirect / query_gateway_child,把用户已保存的设置冲回默认值。
* 刻意不用 ON DUPLICATE 写其它列:那会覆盖
* login_redirect / query_gateway_child / show_child,把用户已保存的设置冲回默认值。
*
* @param userId 用户ID
* @param loginRedirect 新行使用的默认登录跳转
* @param queryGatewayChild 新行使用的默认开关值
* @param showChild 新行使用的默认「包含下级机构数据」值
* @param operator 操作人用户名
* @param now 当前时间
* @return 影响行数(插入 1 / 已存在 0)
@ -59,6 +64,7 @@ public interface SysUserSettingMapper {
int insertDefaultIfAbsent(@Param("userId") Long userId,
@Param("loginRedirect") String loginRedirect,
@Param("queryGatewayChild") Boolean queryGatewayChild,
@Param("showChild") Boolean showChild,
@Param("operator") String operator,
@Param("now") Date now);

View File

@ -25,13 +25,17 @@ public interface ISysUserSettingService {
SysUserSetting selectByUserId(Long userId);
/**
* 保存用户设置(存在则更新,不存在则插入)
* 部分更新用户设置:只写 {@code patch} 中非 null 的字段,未传的字段保持库中原值
*
* @param userId 用户ID
* @param setting 设置内容,userId 字段忽略,以入参 userId 为准
* <p>调用方每次只提交「用户真正改动过」的配置项,因此保存某一项不会把其它项冲成默认值。
* 用户还没有设置行时先落一行默认值再更新,避免 UPDATE 0 行静默丢改动。
*
* @param userId 用户ID
* @param patch 只填本次要改的字段,userId 字段忽略,以入参 userId 为准
* @param operator 操作人用户名,写入 update_by
* @return 影响行数
*/
int save(Long userId, SysUserSetting setting);
int updatePartial(Long userId, SysUserSetting patch, String operator);
/**
* 查询登录落地页候选项

View File

@ -37,7 +37,9 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService {
*
* <p>用 \n 而不是常见的逗号/竖线:loginRedirect 是路由 path 不含换行,
* pinnedTags 是 JSON 数组(元素为带引号的字符串)也不含裸换行,
* queryGatewayChild 是布尔字面量,三段互不冲突。
* showChild 与 queryGatewayChild 是布尔字面量,四段互不冲突。
* <p>字段顺序固定为:loginRedirect | pinnedTags | showChild | queryGatewayChild,
* 与 {@link #parseCached} 一一对应,增删字段必须同步改这两处。
*/
private static final String CACHE_SEPARATOR = "\n";
@ -80,15 +82,17 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService {
redisCache.setStrCacheObject(cacheKey,
setting.getLoginRedirect() + CACHE_SEPARATOR
+ setting.getPinnedTags() + CACHE_SEPARATOR
+ setting.getShowChild() + CACHE_SEPARATOR
+ setting.getQueryGatewayChild());
}
/**
* 解析缓存值,格式非法时返回 null 走查库
*
* <p>段数必须严格等于 3:旧版本只缓存了 2 段(loginRedirect + pinnedTags),
* 若按缺省值补齐会读到错误的 queryGatewayChild,故旧格式一律判为未命中,
* 由查库回填新格式。缓存无 TTL,但每个用户最多回填一次。
* <p>段数必须严格等于 4:历史版本只缓存了 2 段(loginRedirect + pinnedTags)、
* 后又扩到 3 段(追加 queryGatewayChild)。若按缺省值补齐,会把旧格式里
* 那个位置的字段当成 showChild 读出来,故段数不等一律判为未命中,由查库回填新格式。
* 缓存无 TTL,但每个用户最多回填一次。
*
* @param cached 缓存字符串
* @param userId 用户ID
@ -96,38 +100,33 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService {
*/
private SysUserSetting parseCached(String cached, Long userId) {
String[] parts = cached.split(CACHE_SEPARATOR, -1);
if (parts.length != 3) {
if (parts.length != 4) {
return null;
}
SysUserSetting setting = SysUserSetting.defaultOf(userId);
setting.setLoginRedirect(parts[0]);
setting.setPinnedTags(parts[1]);
setting.setQueryGatewayChild(Boolean.parseBoolean(parts[2]));
setting.setShowChild(Boolean.parseBoolean(parts[2]));
setting.setQueryGatewayChild(Boolean.parseBoolean(parts[3]));
return setting;
}
@Override
@DS("master")
public int save(Long userId, SysUserSetting setting) {
SysUserSetting target = SysUserSetting.defaultOf(userId);
target.setLoginRedirect(setting.getLoginRedirect());
// 未传时落到默认值:列约束为 NOT NULL,直接写 null 会在 STRICT 模式下报错
target.setQueryGatewayChild(setting.getQueryGatewayChild() == null
? UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD
: setting.getQueryGatewayChild());
target.setRemark(setting.getRemark());
@Transactional(rollbackFor = Exception.class)
public int updatePartial(Long userId, SysUserSetting patch, String operator) {
Date now = new Date();
target.setUpdateBy(setting.getUpdateBy());
target.setUpdateTime(now);
// create_by/create_time 仅首次写入,由 ON DUPLICATE KEY UPDATE 的列清单保证不被覆盖
target.setCreateBy(setting.getUpdateBy());
target.setCreateTime(now);
int rows = userSettingMapper.upsert(target);
// 写库成功后立即刷缓存。必须重查库再回填:upsert 的 ON DUPLICATE 子句不更新
// pinned_tags,而内存对象 target 的 pinnedTags 是默认空数组,
// 直接回填会把用户的固定标签"洗"成空。
refreshCache(userId, target);
// 行必须先存在,否则后面的部分更新会 UPDATE 0 行、静默丢掉本次改动
userSettingMapper.insertDefaultIfAbsent(userId, UserSettingConstants.DEFAULT_LOGIN_REDIRECT,
UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, UserSettingConstants.DEFAULT_SHOW_CHILD,
operator, now);
patch.setUserId(userId);
patch.setUpdateBy(operator);
patch.setUpdateTime(now);
int rows = userSettingMapper.updateSetting(patch);
// 写库成功后重查库再回填缓存:patch 里只有本次改动的字段,
// 直接拿它刷缓存会把未改动字段写成 null。
refreshCache(userId, patch);
return rows;
}
@ -141,7 +140,8 @@ public class SysUserSettingServiceImpl implements ISysUserSettingService {
Date now = new Date();
// 行必须先存在,后面的锁定读才有东西可锁
userSettingMapper.insertDefaultIfAbsent(userId, UserSettingConstants.DEFAULT_LOGIN_REDIRECT,
UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, operator, now);
UserSettingConstants.DEFAULT_QUERY_GATEWAY_CHILD, UserSettingConstants.DEFAULT_SHOW_CHILD,
operator, now);
List<String> paths = parsePinnedTags(userSettingMapper.selectPinnedTagsForUpdate(userId));
if (paths.contains(path)) {
// 已固定:幂等返回。不重复追加,也不把它挪到末尾,避免重复点击改变固定区顺序

View File

@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="loginRedirect" column="login_redirect" />
<result property="pinnedTags" column="pinned_tags" />
<result property="queryGatewayChild" column="query_gateway_child" />
<result property="showChild" column="show_child" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectUserSettingVo">
select setting_id, user_id, login_redirect, pinned_tags, query_gateway_child,
select setting_id, user_id, login_redirect, pinned_tags, query_gateway_child, show_child,
create_by, create_time, update_by, update_time, remark
from sys_user_setting
</sql>
@ -34,37 +35,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<!--
user_id 上有唯一索引 uk_sys_user_setting_user,
用 INSERT ... ON DUPLICATE KEY UPDATE 收敛「先查后插」的并发竞态:
并发首次保存不会因唯一键冲突抛 DuplicateKey,也不需要 SELECT FOR UPDATE。
create_time 保留首次写入时间,重复插入时不覆盖。
pinned_tags 故意不在此更新:它由 tags-view 的独立写入路径维护,
并入 upsert 会导致保存其它设置时把用户的固定标签冲成默认值。
部分更新:只更新**显式传入**(非 null)的列。
设置页每次只提交用户真正改动过的那几项,因此保存 A 配置不可能把 B 配置冲成默认值——
这是把写入从「整体覆盖」拆成「单量提交」的关键:
· Boolean 为 null = 本次不改这一项(显式传 false 与不传是两回事,包装类型能区分)
· String 为 null = 本次不改这一项
刻意不碰 pinned_tags:它由 tags-view 的独立写入路径维护,混进来会互相覆盖。
调用方必须先 insertDefaultIfAbsent 保证行存在,否则 UPDATE 0 行会静默丢改动。
-->
<insert id="upsert" parameterType="com.fastbee.system.domain.SysUserSetting">
insert into sys_user_setting
(user_id, login_redirect, pinned_tags, query_gateway_child, create_by, create_time, update_by, update_time, remark)
values
(#{userId}, #{loginRedirect}, #{pinnedTags}, #{queryGatewayChild}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{remark})
on duplicate key update
login_redirect = values(login_redirect),
query_gateway_child = values(query_gateway_child),
update_by = values(update_by),
update_time = values(update_time),
remark = values(remark)
</insert>
<update id="updateSetting" parameterType="com.fastbee.system.domain.SysUserSetting">
update sys_user_setting
<set>
<if test="loginRedirect != null">login_redirect = #{loginRedirect},</if>
<if test="queryGatewayChild != null">query_gateway_child = #{queryGatewayChild},</if>
<if test="showChild != null">show_child = #{showChild},</if>
update_by = #{updateBy},
update_time = #{updateTime}
</set>
where user_id = #{userId}
</update>
<!--
确保设置行存在:不存在则插入默认行,已存在则**什么都不改**。
固定标签的增量写入要在一行上做「锁定读 → 改 → 写」,行必须先存在。
刻意不用 upsert:它的 ON DUPLICATE 子句会覆盖 login_redirect / query_gateway_child,
两处调用:① 固定标签的增量写入要在一行上做「锁定读 → 改 → 写」,行必须先存在;
② 设置页的部分更新前先兜一行,否则 UPDATE 0 行会静默丢改动。
刻意不用 ON DUPLICATE 写其它列:那会覆盖 login_redirect / query_gateway_child / show_child,
把用户已保存的设置冲回默认值;这里用 user_id = user_id 作为 no-op 更新。
-->
<insert id="insertDefaultIfAbsent">
insert into sys_user_setting
(user_id, login_redirect, pinned_tags, query_gateway_child, create_by, create_time, update_by, update_time)
(user_id, login_redirect, pinned_tags, query_gateway_child, show_child, create_by, create_time, update_by, update_time)
values
(#{userId}, #{loginRedirect}, '[]', #{queryGatewayChild}, #{operator}, #{now}, #{operator}, #{now})
(#{userId}, #{loginRedirect}, '[]', #{queryGatewayChild}, #{showChild}, #{operator}, #{now}, #{operator}, #{now})
on duplicate key update
user_id = user_id
</insert>
@ -82,8 +84,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<!--
整列覆盖写固定标签。与登录跳转是两条独立写入路径:若合并进 upsert,
保存登录跳转时会把固定标签一并覆盖成默认值。
整列覆盖写固定标签。与设置页的部分更新是两条独立写入路径:
若把 pinned_tags 并进 updateSetting,保存登录跳转等配置时会把固定标签一并覆盖掉。
**只在 addPinnedTag / removePinnedTag 的事务内调用**,且在 SELECT ... FOR UPDATE
之后落下,入参是锁内读出并改好的完整数组;对外不再有全量覆盖写的入口。
-->

View File

@ -36,6 +36,10 @@
-- ADD COLUMN `query_gateway_child` tinyint(1) NOT NULL DEFAULT 0
-- COMMENT '按编号查询时是否查网关子设备(0否1是)'
-- AFTER `pinned_tags`;
-- ALTER TABLE `sys_user_setting`
-- ADD COLUMN `show_child` tinyint(1) NOT NULL DEFAULT 0
-- COMMENT '设备管理列表是否包含下级机构数据(0否1是)'
-- AFTER `query_gateway_child`;
--
-- query_gateway_child:开启后 /iot/device/shortList **按设备编号查询时**(serialNumber 非空):
-- ① 命中的若是网关子设备编号,返回其所属的网关设备(子设备 gw_dev_code 存的就是网关 serial_number,
@ -46,6 +50,16 @@
-- 所以短列表始终只出顶层设备(网关 + 直连),既不出现挂在网关下的真子设备,
-- 默认列表也不会多出 device_type=4 的设备。
-- 默认 0。读该值的入口是 DeviceController#shortList,有 SecurityContext,故为 per-user 设置。
--
-- show_child:设备管理列表是否包含下级机构数据(原为设备管理页勾选框,值存在浏览器
-- localStorage 的 device-showChild,换浏览器/换电脑就丢,现改为落库)。
-- 生效条件同 Device.showChild:仅在 deptId 非空(机构用户)时参与 SQL,语义见
-- DeviceMapper.xml#selectDeviceShortListJoinWhere —— 开启时 tenant_id 落在「本机构
-- 及全部下级机构用户」集合内,关闭时仅本机构。
-- 该值现在由「系统管理 → 系统设置」页维护,设备管理页只读不写(页面内的勾选框已移除)。
-- 设备管理页进入时读一次该设置并作为查询参数下发,故本列是「前端读取、后端不感知」的偏好,
-- DeviceController 在未收到 showChild 时依然按 false 处理。
-- 默认 0 = 与引入本开关前完全一致。
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `sys_user_setting` (
`setting_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '设置主键',
@ -53,6 +67,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_setting` (
`login_redirect` varchar(128) NOT NULL DEFAULT '/bigscreen' COMMENT '登录后跳转路由path',
`pinned_tags` text NOT NULL DEFAULT ('[]') COMMENT '任务栏固定标签JSON数组,按序存 path;空数组表示未固定',
`query_gateway_child` tinyint(1) NOT NULL DEFAULT 0 COMMENT '按编号查询时是否查网关子设备(0否1是)',
`show_child` tinyint(1) NOT NULL DEFAULT 0 COMMENT '设备管理列表是否包含下级机构数据(0否1是)',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',

View File

@ -8,11 +8,13 @@ export function getUserSetting() {
})
}
// 保存当前用户的设置
// 部分更新当前用户的设置:data 里只放**本次改动过**的字段,未改动的一律不放
// 用 PATCH 而非 PUT:服务端只按非空字段逐列更新,未传的字段保持库中原值,
// 这样保存某一项配置不会把其它配置冲回默认值
export function updateUserSetting(data) {
return request({
url: '/system/userSetting',
method: 'put',
method: 'patch',
data: data
})
}

View File

@ -120,13 +120,6 @@
</el-dropdown-menu>
</el-dropdown>
</el-col>
<el-col :span="1.5" style="line-height: 28px">
<el-checkbox v-model="queryParams.showChild" style="margin: 0px 10px" @change="handleQuery">
{{ $t('device.index.105953-18') }}
</el-checkbox>
<el-tooltip :content="$t('device.index.105953-19')" placement="top"><i class="el-icon-question"></i>
</el-tooltip>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList">
<template>
<el-tooltip effect="dark" content="切换卡片/列表" placement="top">
@ -401,6 +394,7 @@ import vueQr from 'vue-qr';
import {listDeviceShort, delDevice} from '@/api/iot/device';
import {listGroup} from '@/api/iot/group';
import {deptsTreeSelect} from '@/api/system/user';
import {getUserSetting} from '@/api/system/userSetting';
import {delSipDeviceBySipId} from '@/api/iot/sipdevice';
import auth from '@/plugins/auth';
import Treeselect from '@riophae/vue-treeselect';
@ -447,12 +441,16 @@ export default {
myGroupList: [],
// 机构树
deptOptions: [],
// 「是否包含下级机构数据」是否已从个人设置同步完成(首次同步由 created 负责)
showChildLoaded: false,
// 根路径
baseUrl: process.env.VUE_APP_BASE_API,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 12,
// 「是否包含下级机构数据」由个人设置(系统管理 → 系统设置)决定,
// 进入页面时由 syncShowChild() 覆盖,页面内不再就地修改
showChild: false,
entrance: 1,
deviceName: null,
@ -526,13 +524,13 @@ export default {
this.queryParams.productId = null;
this.queryParams.groupId = null;
}
// 恢复"显示下级机构数据"用户选择
const cachedShowChild = this.$cache.local.get('device-showChild');
if (cachedShowChild === true || cachedShowChild === 'true') {
this.queryParams.showChild = true;
}
this.getDeptTree();
this.connectMqtt();
// 先把「是否包含下级机构数据」从个人设置同步过来再拉列表,
// 避免先用默认值查一次、拿到设置后又重查一次的闪动
this.syncShowChild().then(() => {
this.showChildLoaded = true;
this.connectMqtt();
});
},
activated() {
// 页码筛选(优先恢复页码,无论是否有其他参数变化)
@ -580,8 +578,46 @@ export default {
// 浏览器回退时,即使没有 t 参数也要刷新列表
this.getList();
}
this.resyncShowChild();
},
methods: {
/**
* 同步「设备管理是否包含下级机构数据」
*
* 该偏好由系统设置页维护并落 sys_user_setting,本页只读不写。
* 拉取失败时保持默认 false(不写坏值),并返回是否发生了变化,
* 供 activated 判断是否需要重查列表。
*
* @returns {Promise<boolean>} 值是否发生变化
*/
syncShowChild() {
return getUserSetting()
.then((response) => {
const next = !!(response.data && response.data.showChild);
const changed = this.queryParams.showChild !== next;
this.queryParams.showChild = next;
return changed;
})
.catch(() => false);
},
/**
* 页面被重新激活时同步偏好
*
* 本页被 keep-alive 常驻时切回来不会重跑 created,若不在激活时重新同步,
* 在系统设置页改了开关再切回本页将看不到变化。
* 首次进入由 created 负责(它同步完才拉列表),此处跳过,
* 否则会和 created 抢跑、让列表在 MQTT 连上之前就查一次。
*/
resyncShowChild() {
if (!this.showChildLoaded) {
return;
}
this.syncShowChild().then((changed) => {
if (changed) {
this.getList();
}
});
},
/* 连接Mqtt消息服务器 */
async connectMqtt() {
if (this.$mqttTool.client == null) {
@ -795,7 +831,6 @@ export default {
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.$cache.local.set('device-showChild', this.queryParams.showChild);
this.getList();
},
/** 重置按钮操作 */
@ -804,8 +839,8 @@ export default {
this.queryParams.groupId = null;
this.queryParams.serialNumber = null;
this.queryParams.deptId = null;
this.queryParams.showChild = false;
this.$cache.local.remove('device-showChild');
// 刻意不重置 showChild:「是否包含下级机构数据」是个人设置里的持久偏好,
// 重置按钮只管本次查询条件,改了偏好下次进页面又会被覆盖回来,反而不一致
this.resetForm('queryForm');
this.handleQuery();
},

View File

@ -21,9 +21,13 @@
<el-switch v-model="form.queryGatewayChild" />
</setting-row>
<setting-row label="设备管理查下级机构" desc="开启后设备管理列表返回本机构及全部下级机构的设备,关闭时仅返回本机构设备。仅对归属了机构的账号生效;该值即设备管理页原先的「显示下级机构数据」勾选项,页面内不再就地切换。">
<el-switch v-model="form.showChild" />
</setting-row>
<div class="setting-actions">
<el-button type="primary" icon="el-icon-check" :loading="saving" @click="submitForm">保存</el-button>
<el-button icon="el-icon-refresh-left" @click="handleReset">重置</el-button>
<span class="setting-actions__hint">仅提交改动过的配置项</span>
</div>
</div>
</template>
@ -37,10 +41,11 @@ import {
import SettingRow from './SettingRow';
// 默认值与后端 UserSettingConstants 保持一致:
// 未指定的账号保持原有的数据大屏落地行为;子设备开关默认关闭
// 未指定的账号保持原有的数据大屏落地行为;子设备开关与「查下级机构数据」默认关闭
const DEFAULT_FORM = {
loginRedirect: '/bigscreen',
queryGatewayChild: false
queryGatewayChild: false,
showChild: false
};
export default {
@ -51,7 +56,9 @@ export default {
saving: false,
optionsLoading: false,
options: [],
form: { ...DEFAULT_FORM }
form: { ...DEFAULT_FORM },
// 服务端当前值的快照:保存时只提交与它不同的字段,未改动的不进 payload
original: { ...DEFAULT_FORM }
};
},
created() {
@ -73,31 +80,52 @@ export default {
getUserSetting().then((res) => {
const data = res.data || {};
// 后端对未配置过的用户返回默认值,此处仅在值缺失时兜底。
// queryGatewayChild 不能用 ||:布尔 false 会被吞掉,导致开关回显错乱。
this.form = {
// 两个布尔开关不能用 ||:布尔 false 会被吞掉,导致开关回显错乱。
const form = {
loginRedirect: data.loginRedirect || DEFAULT_FORM.loginRedirect,
queryGatewayChild: data.queryGatewayChild === null || data.queryGatewayChild === undefined
? DEFAULT_FORM.queryGatewayChild
: data.queryGatewayChild
: data.queryGatewayChild,
showChild: data.showChild === null || data.showChild === undefined
? DEFAULT_FORM.showChild
: data.showChild
};
this.form = { ...form };
// 快照必须与回显值同源(含兜底后的值),否则未改动也会被当成脏数据提交
this.original = { ...form };
});
},
/** 只取本次真正改动过的字段,未改动的不进 payload */
buildPatch() {
const patch = {};
Object.keys(this.form).forEach((key) => {
if (this.form[key] !== this.original[key]) {
patch[key] = this.form[key];
}
});
return patch;
},
submitForm() {
const patch = this.buildPatch();
const keys = Object.keys(patch);
if (keys.length === 0) {
// 不发明知无改动的请求;服务端也会拒绝空 payload
this.$modal.msgWarning('没有需要保存的改动');
return;
}
this.saving = true;
updateUserSetting({
loginRedirect: this.form.loginRedirect,
queryGatewayChild: this.form.queryGatewayChild
})
updateUserSetting(patch)
.then(() => {
this.$modal.msgSuccess('保存成功,下次登录生效');
// 提交成功后推进快照,保证下一次保存只带新的改动
this.original = { ...this.original, ...patch };
// 「登录后显示页面」下次登录才生效,其余开关下次进入对应页面即生效
this.$modal.msgSuccess(keys.includes('loginRedirect')
? '保存成功,登录后显示页面将于下次登录生效'
: '保存成功');
})
.finally(() => {
this.saving = false;
});
},
handleReset() {
this.form = { ...DEFAULT_FORM };
this.getSetting();
}
}
};
@ -112,5 +140,12 @@ export default {
// 172px = SettingRow 的标签列 160px + 列间距 12px,见 SettingRow.vue
.setting-actions {
padding: 16px 0 0 172px;
&__hint {
margin-left: 10px;
vertical-align: middle;
color: #909399;
font-size: 12px;
}
}
</style>