BUILTIN_REDIRECTS = Collections.unmodifiableList(Arrays.asList(
+ new BuiltinRedirect("/index", "首页"),
+ new BuiltinRedirect("/bigscreen", "数据大屏")
+ ));
+
+ /**
+ * 是否下发读取设备 IMEI/ICCID 指令的全局配置 key
+ *
+ * 读取方为设备上电异步线程,无登录上下文,故存 {@code sys_config} 而非用户设置表。
+ * 取代原实现中运维经 {@code BackendController#/set} 手动写入的裸 Redis key {@code get_imei_iccid}。
+ */
+ public static final String READ_IMEI_ICCID_KEY = "iot.power_on.read_imei_iccid";
+
+ /**
+ * 固定标签的「未固定」空值
+ *
+ *
列约束为 NOT NULL DEFAULT ('[]'),不存 NULL、不存空串。
+ * 存空数组而非 NULL,读侧无需判空即可安全反序列化。
+ */
+ public static final String EMPTY_PINNED_TAGS = "[]";
+
+ private UserSettingConstants() {
+ }
+
+ /**
+ * 内置固定路由项
+ */
+ public static class BuiltinRedirect {
+ private final String path;
+ private final String name;
+
+ public BuiltinRedirect(String path, String name) {
+ this.path = path;
+ this.name = name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisCache.java b/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisCache.java
index d74beacf..f6e6be99 100644
--- a/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisCache.java
+++ b/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisCache.java
@@ -810,6 +810,32 @@ public class RedisCache {
return stringRedisTemplate.opsForValue().get(key);
}
+ /**
+ * 缓存基本的字符串
+ *
+ * 与 {@link #getStrCacheObject(String)} 配套。
+ * 走 StringRedisTemplate 而非 redisTemplate,避免 FastJson2 把值包成带引号的 JSON 字符串,
+ * 导致读回时多一层引号。
+ *
+ * @param key 缓存键值
+ * @param value 缓存值,不能为 null
+ */
+ public void setStrCacheObject(final String key, final String value) {
+ stringRedisTemplate.opsForValue().set(key, value);
+ }
+
+ /**
+ * 缓存基本的字符串并设置过期时间
+ *
+ * @param key 缓存键值
+ * @param value 缓存值,不能为 null
+ * @param timeout 时间
+ * @param timeUnit 时间颗粒度
+ */
+ public void setStrCacheObject(final String key, final String value, final Integer timeout, final TimeUnit timeUnit) {
+ stringRedisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+ }
+
/**
* 删除单个对象
*
diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java
index f4cbc188..3fff51e0 100644
--- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java
+++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/executor/PowerOnTriggerTaskExecutor.java
@@ -3,9 +3,11 @@ package com.fastbee.iot.data.executor;
import com.fastbee.base.service.ISessionStore;
import com.fastbee.base.session.Session;
import com.fastbee.common.constant.FastBeeConstant;
+import com.fastbee.common.constant.UserSettingConstants;
import com.fastbee.common.core.mq.message.DeviceMessage;
import com.fastbee.common.core.protocol.Message;
import com.fastbee.common.core.redis.RedisKeyBuilder;
+import com.fastbee.common.core.text.Convert;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.gateway.protocol.ByteUtils;
import com.fastbee.iot.data.service.IDeviceMessageService;
@@ -15,6 +17,7 @@ import com.fastbee.iot.domain.ModbusJob;
import com.fastbee.iot.enums.DeviceType;
import com.fastbee.iot.service.IDeviceService;
import com.fastbee.iot.service.IModbusJobService;
+import com.fastbee.system.service.ISysConfigService;
import io.netty.buffer.Unpooled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
@@ -68,6 +71,9 @@ public class PowerOnTriggerTaskExecutor {
@Resource
private IDeviceService deviceService;
+ @Resource
+ private ISysConfigService sysConfigService;
+
/**
* 每台设备同一时刻仅允许一个"上电触发轮询"流水线运行,避免重复触发/并发下发导致应答错配
*/
@@ -90,8 +96,9 @@ public class PowerOnTriggerTaskExecutor {
//log.debug("设置上电触发标识,设备:{}", serialNumber);
// 2、下发IMEI/ICCID读取指令
- if (redisTemplate.opsForValue().get("get_imei_iccid") == null){
- log.warn("无缓存标识,无法下发IMEI/ICCID指令");
+ // 开关为全局配置:读该值的线程来自设备注册(@Async),无登录用户上下文,故不落用户设置表
+ if (!isReadImeiIccidEnabled()) {
+ log.info("IMEI/ICCID读取指令已关闭,跳过下发,设备编号:{}", serialNumber);
return;
}
Device device = deviceService.selectDeviceInfoBySerialNumber(serialNumber);
@@ -131,6 +138,22 @@ public class PowerOnTriggerTaskExecutor {
}
}
+ /**
+ * 读取「是否下发IMEI/ICCID指令」全局开关
+ *
+ *
配置缺失时按开启处理:设备出厂时 imei/iccid/经纬度均为空,
+ * 不下发则该字段永远无法填充,功能形同失效。
+ *
+ * @return true 下发,false 不下发
+ */
+ private boolean isReadImeiIccidEnabled() {
+ String configValue = sysConfigService.selectConfigByKey(UserSettingConstants.READ_IMEI_ICCID_KEY);
+ if (StringUtils.isEmpty(configValue)) {
+ return true;
+ }
+ return Convert.toBool(configValue);
+ }
+
/**
* 检查并消费上电触发标识(原子操作)
* @param serialNumber 设备编号
diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/LoginRedirectOption.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/LoginRedirectOption.java
new file mode 100644
index 00000000..f10531e9
--- /dev/null
+++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/LoginRedirectOption.java
@@ -0,0 +1,50 @@
+package com.fastbee.system.domain;
+
+import java.io.Serializable;
+
+/**
+ * 登录落地页候选项(系统菜单)
+ *
+ *
数据源为 {@code sys_menu} 中可作为落地页的页签菜单,供用户选择登录后进入的页面。
+ * 已剔除按钮(F)、停用(status=1)、隐藏(visible=1) 与外链(is_frame=0)菜单——
+ * 外链若进入本列表会成为开放重定向入口。
+ *
+ * @author Sumuxi
+ */
+public class LoginRedirectOption implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 完整路由 path,如 /iot/device
+ */
+ private String path;
+
+ /**
+ * 菜单名称
+ */
+ private String menuName;
+
+ public LoginRedirectOption() {
+ }
+
+ public LoginRedirectOption(String path, String menuName) {
+ this.path = path;
+ this.menuName = menuName;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getMenuName() {
+ return menuName;
+ }
+
+ public void setMenuName(String menuName) {
+ this.menuName = menuName;
+ }
+}
diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java
new file mode 100644
index 00000000..3e73555a
--- /dev/null
+++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/domain/SysUserSetting.java
@@ -0,0 +1,111 @@
+package com.fastbee.system.domain;
+
+import com.fastbee.common.constant.UserSettingConstants;
+import com.fastbee.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 用户系统设置对象 sys_user_setting
+ *
+ *
一行一用户(user_id 唯一)。所有设置列 NOT NULL + DEFAULT,
+ * 不存在「未配置过」的 NULL 态;业务默认值见 {@link UserSettingConstants}。
+ *
+ * @author Sumuxi
+ */
+public class SysUserSetting extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 设置主键
+ */
+ @ApiModelProperty(value = "设置主键")
+ private Long settingId;
+
+ /**
+ * 用户ID
+ */
+ @ApiModelProperty(value = "用户ID")
+ private Long userId;
+
+ /**
+ * 登录后跳转路由 path
+ */
+ @ApiModelProperty(value = "登录后跳转路由path")
+ private String loginRedirect;
+
+ /**
+ * 任务栏固定标签,JSON 数组字符串,按固定顺序存放 route path
+ *
+ *
只存用户右键「固定标签到任务栏」的 pinned 集合;
+ * 路由自身声明的 affix(如首页)由 constantRoutes 天然重建,不在此列。
+ * 空数组字符串 "[]" 表示未固定,不存 NULL。
+ */
+ @ApiModelProperty(value = "任务栏固定标签JSON数组")
+ private String pinnedTags;
+
+ public SysUserSetting() {
+ }
+
+ /**
+ * 新建一份带默认值的设置(用户首次保存时使用)
+ *
+ * @param userId 用户ID
+ * @return 默认设置对象
+ */
+ public static SysUserSetting defaultOf(Long userId) {
+ SysUserSetting setting = new SysUserSetting();
+ setting.setUserId(userId);
+ setting.setLoginRedirect(UserSettingConstants.DEFAULT_LOGIN_REDIRECT);
+ setting.setPinnedTags(UserSettingConstants.EMPTY_PINNED_TAGS);
+ return setting;
+ }
+
+ public Long getSettingId() {
+ return settingId;
+ }
+
+ public void setSettingId(Long settingId) {
+ this.settingId = settingId;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getLoginRedirect() {
+ return loginRedirect;
+ }
+
+ public void setLoginRedirect(String loginRedirect) {
+ this.loginRedirect = loginRedirect;
+ }
+
+ public String getPinnedTags() {
+ return pinnedTags;
+ }
+
+ public void setPinnedTags(String pinnedTags) {
+ this.pinnedTags = pinnedTags;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+ .append("settingId", getSettingId())
+ .append("userId", getUserId())
+ .append("loginRedirect", getLoginRedirect())
+ .append("pinnedTags", getPinnedTags())
+ .append("createBy", getCreateBy())
+ .append("createTime", getCreateTime())
+ .append("updateBy", getUpdateBy())
+ .append("updateTime", getUpdateTime())
+ .append("remark", getRemark())
+ .toString();
+ }
+}
diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java
new file mode 100644
index 00000000..40f66ea4
--- /dev/null
+++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/mapper/SysUserSettingMapper.java
@@ -0,0 +1,47 @@
+package com.fastbee.system.mapper;
+
+import com.fastbee.system.domain.LoginRedirectOption;
+import com.fastbee.system.domain.SysUserSetting;
+
+import java.util.List;
+
+/**
+ * 用户系统设置 数据层
+ *
+ * @author Sumuxi
+ */
+public interface SysUserSettingMapper {
+
+ /**
+ * 按用户ID查询设置
+ *
+ * @param userId 用户ID
+ * @return 设置,不存在返回 null
+ */
+ SysUserSetting selectByUserId(Long userId);
+
+ /**
+ * 保存设置(存在则更新,不存在则插入)
+ *
+ * @param setting 设置
+ * @return 影响行数
+ */
+ int upsert(SysUserSetting setting);
+
+ /**
+ * 仅更新固定标签列
+ *
+ *
用户尚无记录时更新 0 行,调用方需自行判断是否改用 insert。
+ *
+ * @param setting 设置,userId 与 pinnedTags 必填
+ * @return 影响行数
+ */
+ int updatePinnedTags(SysUserSetting setting);
+
+ /**
+ * 查询登录落地页候选项(可作为页签的菜单)
+ *
+ * @return 候选项列表
+ */
+ List selectLoginRedirectOptions();
+}
diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java
new file mode 100644
index 00000000..5927aac3
--- /dev/null
+++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/ISysUserSettingService.java
@@ -0,0 +1,61 @@
+package com.fastbee.system.service;
+
+import com.fastbee.system.domain.LoginRedirectOption;
+import com.fastbee.system.domain.SysUserSetting;
+
+import java.util.List;
+
+/**
+ * 用户系统设置 服务层
+ *
+ * 所有方法的 userId 均由调用方显式传入,Service 内部不读 SecurityUtils。
+ * 原因:设置读取方不只有 HTTP 请求线程(可拿到 SecurityContext),
+ * 还有设备上电等无登录上下文的线程,不能假设一定存在登录态。
+ *
+ * @author Sumuxi
+ */
+public interface ISysUserSettingService {
+
+ /**
+ * 查询用户设置;未保存过时返回带默认值的对象(不会返回 null)
+ *
+ * @param userId 用户ID
+ * @return 用户设置
+ */
+ SysUserSetting selectByUserId(Long userId);
+
+ /**
+ * 保存用户设置(存在则更新,不存在则插入)
+ *
+ * @param userId 用户ID
+ * @param setting 设置内容,userId 字段忽略,以入参 userId 为准
+ * @return 影响行数
+ */
+ int save(Long userId, SysUserSetting setting);
+
+ /**
+ * 查询登录落地页候选项
+ *
+ * @return 候选项列表
+ */
+ List listLoginRedirectOptions();
+
+ /**
+ * 保存任务栏固定标签
+ *
+ * 只落用户右键固定的 pinned 集合;路由自身声明的 affix 不在其中。
+ *
+ * @param userId 用户ID
+ * @param paths 固定标签的路由 path 列表,按固定顺序
+ * @param operator 操作人用户名,写入 update_by
+ */
+ void savePinnedTags(Long userId, List paths, String operator);
+
+ /**
+ * 查询任务栏固定标签
+ *
+ * @param userId 用户ID
+ * @return 固定标签的路由 path 列表,按固定顺序;未固定时返回空列表
+ */
+ List getPinnedTags(Long userId);
+}
diff --git a/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java
new file mode 100644
index 00000000..d4e0eed6
--- /dev/null
+++ b/fastbee-service/fastbee-system-service/src/main/java/com/fastbee/system/service/impl/SysUserSettingServiceImpl.java
@@ -0,0 +1,228 @@
+package com.fastbee.system.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.fastbee.common.constant.CacheConstants;
+import com.fastbee.common.constant.UserSettingConstants;
+import com.fastbee.common.core.redis.RedisCache;
+import com.fastbee.common.utils.StringUtils;
+import com.fastbee.system.domain.LoginRedirectOption;
+import com.fastbee.system.domain.SysUserSetting;
+import com.fastbee.system.mapper.SysUserSettingMapper;
+import com.fastbee.system.service.ISysUserSettingService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户系统设置 服务层实现
+ *
+ * 读取策略:Redis 缓存未命中则查库并回填,未配置过则返回默认值对象。
+ * 缓存值为空表示「该用户还没有记录」,此时不写入缓存——
+ * 否则用户首次保存前,每次读都会穿透到数据库。
+ *
+ * @author Sumuxi
+ */
+@Slf4j
+@Service
+public class SysUserSettingServiceImpl implements ISysUserSettingService {
+
+ /**
+ * 缓存值的字段分隔符
+ *
+ *
用 \n 而不是常见的逗号/竖线:loginRedirect 是路由 path 不含换行,
+ * pinnedTags 是 JSON 数组(元素为带引号的字符串)也不含裸换行。
+ */
+ private static final String CACHE_SEPARATOR = "\n";
+
+ @Autowired
+ private SysUserSettingMapper userSettingMapper;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @Override
+ @DS("master")
+ public SysUserSetting selectByUserId(Long userId) {
+ if (userId == null) {
+ return SysUserSetting.defaultOf(null);
+ }
+ String cacheKey = getCacheKey(userId);
+ // 缓存的是扁平字符串而非整个 POJO,理由同 SysConfigServiceImpl#selectConfigByKey:
+ // 本项目 Redis 序列化器是 FastJson2 + SupportAutoType,直接缓存继承 BaseEntity
+ // (含 Map params 等字段)的对象,反序列化存在取值不稳与
+ // autoType 风险;缓存两个扁平字段可完全规避。
+ String cached = redisCache.getStrCacheObject(cacheKey);
+ if (StringUtils.isNotEmpty(cached)) {
+ SysUserSetting fromCache = parseCached(cached, userId);
+ if (fromCache != null) {
+ return fromCache;
+ }
+ }
+ SysUserSetting setting = userSettingMapper.selectByUserId(userId);
+ if (setting == null) {
+ return SysUserSetting.defaultOf(userId);
+ }
+ writeCache(cacheKey, setting);
+ return setting;
+ }
+
+ /**
+ * 把设置刷入缓存
+ */
+ private void writeCache(String cacheKey, SysUserSetting setting) {
+ redisCache.setStrCacheObject(cacheKey,
+ setting.getLoginRedirect() + CACHE_SEPARATOR + setting.getPinnedTags());
+ }
+
+ /**
+ * 解析缓存值,格式非法时返回 null 走查库
+ *
+ * @param cached 缓存字符串
+ * @param userId 用户ID
+ * @return 设置对象,解析失败返回 null
+ */
+ private SysUserSetting parseCached(String cached, Long userId) {
+ int idx = cached.indexOf(CACHE_SEPARATOR);
+ if (idx < 0) {
+ return null;
+ }
+ SysUserSetting setting = SysUserSetting.defaultOf(userId);
+ setting.setLoginRedirect(cached.substring(0, idx));
+ setting.setPinnedTags(cached.substring(idx + 1));
+ return setting;
+ }
+
+ @Override
+ @DS("master")
+ public int save(Long userId, SysUserSetting setting) {
+ SysUserSetting target = SysUserSetting.defaultOf(userId);
+ target.setLoginRedirect(setting.getLoginRedirect());
+ target.setRemark(setting.getRemark());
+ 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);
+ // 写库成功后立即刷缓存,避免读到旧值。
+ // 回写时保留库中已有的 pinned_tags:upsert 的 ON DUPLICATE 子句不更新该列,
+ // 内存对象却是默认值,若直接回填缓存会把用户的固定标签"洗"成空数组。
+ SysUserSetting latest = userSettingMapper.selectByUserId(userId);
+ writeCache(getCacheKey(userId), latest == null ? target : latest);
+ return rows;
+ }
+
+ @Override
+ @DS("master")
+ public void savePinnedTags(Long userId, List paths, String operator) {
+ String json = normalizePinnedTags(paths);
+ Date now = new Date();
+
+ // 先判断记录是否存在,而不是靠 update 的影响行数:
+ // MySQL 在「值未变化」时 affected_rows 同样为 0,与「记录不存在」无法区分,
+ // 靠返回值判断会在重复提交同一份数据时误走 insert 分支。
+ SysUserSetting exists = userSettingMapper.selectByUserId(userId);
+ SysUserSetting patch = SysUserSetting.defaultOf(userId);
+ patch.setPinnedTags(json);
+ patch.setUpdateBy(operator);
+ patch.setUpdateTime(now);
+
+ if (exists == null) {
+ // 首次固定标签:用户还没有设置记录,走 upsert 而非裸 insert,避免并发下撞唯一键 1062
+ patch.setLoginRedirect(UserSettingConstants.DEFAULT_LOGIN_REDIRECT);
+ patch.setCreateBy(operator);
+ patch.setCreateTime(now);
+ userSettingMapper.upsert(patch);
+ } else {
+ userSettingMapper.updatePinnedTags(patch);
+ }
+ // 回缓存:整体重新查一次,保证与库内最终状态一致
+ SysUserSetting latest = userSettingMapper.selectByUserId(userId);
+ writeCache(getCacheKey(userId), latest == null ? patch : latest);
+ }
+
+ @Override
+ public List getPinnedTags(Long userId) {
+ return parsePinnedTags(selectByUserId(userId).getPinnedTags());
+ }
+
+ /**
+ * 校验并序列化固定标签列表
+ *
+ * 只接受站内绝对路径,去重且保持前端给定的固定顺序。
+ * path 最终会进 router.push,混入外链会变成开放重定向入口。
+ *
+ * @param paths 前端提交的 path 列表
+ * @return JSON 数组字符串
+ */
+ private String normalizePinnedTags(List paths) {
+ List valid = new ArrayList<>();
+ if (paths != null) {
+ for (String path : paths) {
+ if (StringUtils.isEmpty(path) || !path.startsWith("/")) {
+ continue;
+ }
+ if (path.startsWith("//") || path.contains("://") || path.contains("..")) {
+ continue;
+ }
+ if (!valid.contains(path)) {
+ valid.add(path);
+ }
+ }
+ }
+ return JSON.toJSONString(valid);
+ }
+
+ /**
+ * 解析固定标签,脏数据降级为空列表
+ *
+ * 列是 TEXT,允许人工改过;解析失败不应让整个设置页报错。
+ *
+ * @param json JSON 数组字符串
+ * @return path 列表
+ */
+ private List parsePinnedTags(String json) {
+ if (StringUtils.isEmpty(json)) {
+ return new ArrayList<>();
+ }
+ try {
+ List list = JSON.parseArray(json, String.class);
+ return list == null ? new ArrayList<>() : list;
+ } catch (Exception e) {
+ log.warn("解析 pinned_tags 失败,降级为空列表,原始值:{}", json);
+ return new ArrayList<>();
+ }
+ }
+
+ @Override
+ @DS("master")
+ public List listLoginRedirectOptions() {
+ List options = new ArrayList<>();
+ // 内置固定路由优先:/index 与 /bigscreen 由前端 constantRoutes 注册,不在 sys_menu 中
+ for (UserSettingConstants.BuiltinRedirect builtin : UserSettingConstants.BUILTIN_REDIRECTS) {
+ options.add(new LoginRedirectOption(builtin.getPath(), builtin.getName()));
+ }
+ List menuOptions = userSettingMapper.selectLoginRedirectOptions();
+ if (menuOptions != null && !menuOptions.isEmpty()) {
+ for (LoginRedirectOption option : menuOptions) {
+ // 菜单若与内置路由同名路径,以后者为准并去重,避免下拉出现重复项
+ boolean duplicated = options.stream().anyMatch(o -> o.getPath().equals(option.getPath()));
+ if (!duplicated) {
+ options.add(option);
+ }
+ }
+ }
+ return options;
+ }
+
+ private String getCacheKey(Long userId) {
+ return CacheConstants.SYS_USER_SETTING_KEY + userId;
+ }
+}
diff --git a/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml
new file mode 100644
index 00000000..6477db12
--- /dev/null
+++ b/fastbee-service/fastbee-system-service/src/main/resources/mapper/system/SysUserSettingMapper.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select setting_id, user_id, login_redirect, pinned_tags,
+ create_by, create_time, update_by, update_time, remark
+ from sys_user_setting
+
+
+
+
+
+
+ insert into sys_user_setting
+ (user_id, login_redirect, pinned_tags, create_by, create_time, update_by, update_time, remark)
+ values
+ (#{userId}, #{loginRedirect}, #{pinnedTags}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{remark})
+ on duplicate key update
+ login_redirect = values(login_redirect),
+ update_by = values(update_by),
+ update_time = values(update_time),
+ remark = values(remark)
+
+
+
+
+ update sys_user_setting
+ set pinned_tags = #{pinnedTags},
+ update_by = #{updateBy},
+ update_time = #{updateTime}
+ where user_id = #{userId}
+
+
+
+
+
+
diff --git a/sql/sys_user_setting.sql b/sql/sys_user_setting.sql
new file mode 100644
index 00000000..3b6fb968
--- /dev/null
+++ b/sql/sys_user_setting.sql
@@ -0,0 +1,80 @@
+-- =============================================
+-- 用户系统设置(用户维度)+ 设备侧全局开关入库
+-- 维度判据:读的时候拿不拿得到 userId
+-- · 拿得到(HTTP 请求,有 SecurityContext)→ sys_user_setting,per-user
+-- · 拿不到(设备上电 @Async 线程)→ sys_config,全局
+-- 配套代码:SysUserSetting* / PowerOnTriggerTaskExecutor:93
+-- 版本: 1.0 2026-10-05
+-- =============================================
+
+-- ------------------------------------------------------------
+-- 一、用户维度设置表
+-- 一行一用户;所有设置列 NOT NULL + DEFAULT,不设「未配置过」这种 NULL 态
+-- 业务默认值由 Java 常量兜底(UserSettingConstants),不依赖 DB 兜底
+--
+-- pinned_tags:任务栏「固定标签到任务栏」的 JSON 数组,按固定顺序存 route path。
+-- * 原实现存浏览器 localStorage(key: pinned-tags),换浏览器/换电脑就丢,
+-- 现改为落库,同一账号在任何设备登录都能恢复固定区。
+-- * 只存用户右键固定的 pinned 集合;路由自身声明的 affix(首页,router/index.js:80)
+-- 由 constantRoutes 天然重建,不入此列。
+-- * DEFAULT ('[]') 是 MySQL 8.0.13+ 的表达式默认值。TEXT 列不能带字面量 DEFAULT,
+-- 不给默认值时 STRICT_TRANS_TABLES 下会直接报错,故必须显式写空数组。
+-- 低版本数据库请改为 VARCHAR(2000) NOT NULL DEFAULT '[]'。
+--
+-- 已建过表的库用下面的 ALTER 补列(可重复执行前请先确认列不存在):
+-- ALTER TABLE `sys_user_setting`
+-- ADD COLUMN `pinned_tags` text NOT NULL DEFAULT ('[]')
+-- COMMENT '任务栏固定标签JSON数组,按序存 path;空数组表示未固定'
+-- AFTER `login_redirect`;
+-- ------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `sys_user_setting` (
+ `setting_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '设置主键',
+ `user_id` bigint(20) NOT NULL COMMENT '用户ID(sys_user.user_id)',
+ `login_redirect` varchar(128) NOT NULL DEFAULT '/bigscreen' COMMENT '登录后跳转路由path',
+ `pinned_tags` text NOT NULL DEFAULT ('[]') COMMENT '任务栏固定标签JSON数组,按序存 path;空数组表示未固定',
+ `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`setting_id`) USING BTREE,
+ UNIQUE KEY `uk_sys_user_setting_user` (`user_id`) USING BTREE COMMENT '每用户仅一行'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
+ ROW_FORMAT=DYNAMIC COMMENT='用户系统设置表';
+
+-- ------------------------------------------------------------
+-- 二、设备侧全局开关 → 复用已有 sys_config 表
+-- 原实现:PowerOnTriggerTaskExecutor 直读运维手动写入的裸 Redis key
+-- "get_imei_iccid"(经 BackendController /set 写入)
+-- 新实现:改读 sys_config,运维改用已有的 PUT /system/config
+-- config_type='Y' = 系统内置,deleteConfigByIds 会拒删
+-- ------------------------------------------------------------
+INSERT INTO `sys_config`
+ (`config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `remark`)
+SELECT '是否下发读取设备IMEI/ICCID指令', 'iot.power_on.read_imei_iccid', 'true', 'Y', 'system', NOW(),
+ '设备上电后是否下发 AT+IMEI/AT+ICCID/AT+LBS 指令读取;读该值的线程无登录上下文,故为全局配置'
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_config` WHERE `config_key` = 'iot.power_on.read_imei_iccid'
+);
+
+-- ------------------------------------------------------------
+-- 三、菜单:系统管理 → 个人设置
+-- 不设 system:setting:list 权限点的原因:
+-- 接口读写的是「当前登录用户自己的」设置,无需菜单级授权;
+-- 若填了权限点,非管理员账号将看不到也用不了自己的设置页。
+-- menu_type='C' 菜单,component 指向新建的 views/system/setting/index.vue
+-- ------------------------------------------------------------
+INSERT INTO `sys_menu`
+ (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`)
+SELECT '系统设置', 1, 13, 'setting', 'system/setting/index', 1, 0, 'C', '0', '0', '', 'setting', 'system', NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_menu` WHERE `parent_id` = 1 AND `path` = 'setting'
+);
+
+-- ------------------------------------------------------------
+-- 四、清理历史遗留的裸 Redis key
+-- 原开关 key 无前缀、不在任何 key 生成器中,与 RedisKeyBuilder 规范不符。
+-- 确认线上设备已完成一轮上电、开关已由 sys_config 接管后再执行:
+-- redis-cli DEL get_imei_iccid
+-- 切勿在切换完成前执行,否则 IMEI/ICCID 指令会立即停止下发。
+-- ------------------------------------------------------------
diff --git a/yack-web/src/api/system/userSetting.js b/yack-web/src/api/system/userSetting.js
new file mode 100644
index 00000000..7791f5f4
--- /dev/null
+++ b/yack-web/src/api/system/userSetting.js
@@ -0,0 +1,43 @@
+import request from '@/utils/request'
+
+// 获取当前用户的设置
+export function getUserSetting() {
+ return request({
+ url: '/system/userSetting',
+ method: 'get'
+ })
+}
+
+// 保存当前用户的设置
+export function updateUserSetting(data) {
+ return request({
+ url: '/system/userSetting',
+ method: 'put',
+ data: data
+ })
+}
+
+// 获取登录落地页候选项(取自系统菜单)
+export function listLoginRedirectOptions() {
+ return request({
+ url: '/system/userSetting/loginRedirectOptions',
+ method: 'get'
+ })
+}
+
+// 获取当前用户固定的任务栏标签
+export function getPinnedTags() {
+ return request({
+ url: '/system/userSetting/pinnedTags',
+ method: 'get'
+ })
+}
+
+// 保存当前用户固定的任务栏标签
+export function updatePinnedTags(paths) {
+ return request({
+ url: '/system/userSetting/pinnedTags',
+ method: 'put',
+ data: paths
+ })
+}
diff --git a/yack-web/src/layout/components/TagsView/index.vue b/yack-web/src/layout/components/TagsView/index.vue
index 4b33af48..08cf470b 100644
--- a/yack-web/src/layout/components/TagsView/index.vue
+++ b/yack-web/src/layout/components/TagsView/index.vue
@@ -88,6 +88,23 @@ const MORE_PANEL_WIDTH = 200
// 鼠标横向位移超过这个像素才算拖拽,否则仍按普通点击处理
const DRAG_THRESHOLD = 4
+/**
+ * 归一化路由 path:补齐前导斜杠、根路径归一为 '/'
+ *
+ * $route.path 形如 '/iot/device',而 filterAsyncRouter 生成的子路由 path
+ * 形如 'iot/device'(相对父级)。比较路由时必须先归一化,否则匹配不到。
+ *
+ * @param {string} path 原始 path
+ * @returns {string} 归一化后的 path,无效输入返回空串
+ */
+function normalizePath(path) {
+ if (!path || typeof path !== 'string') {
+ return ''
+ }
+ const trimmed = path.replace(/^\/+/, '')
+ return '/' + trimmed
+}
+
export default {
components: { ScrollPane },
data() {
@@ -264,14 +281,76 @@ export default {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
- // 2. 恢复上次会话「固定到任务栏」的标签(localStorage),保证刷新/重新登录后仍在固定区
- this.$store.state.tagsView.pinnedViews
- .filter(tag => tag.name && !affixPaths.includes(tag.path))
- .forEach(tag => {
- this.$store.dispatch('tagsView/addView', Object.assign({}, tag, {
- meta: Object.assign({}, tag.meta, { affix: true, pinned: true })
- }))
- })
+ // 2. 从后端恢复「固定到任务栏」的标签,跨浏览器/跨设备生效。
+ // 接口异步,必须等返回后再补建固定区,否则 mounted 阶段读到空列表就再也不会重建。
+ this.$store.dispatch('tagsView/loadPinnedViews').then(paths => {
+ paths
+ // 路由里已不存在该 path(菜单被删/改名)时丢弃,避免产生打不开的死标签
+ .map(path => this.findRouteByPath(path))
+ .filter(Boolean)
+ .forEach(({ route, path: fullPath }) => {
+ // 路由自身声明的 affix 已在上一步加入,不重复添加
+ if (!route.name || affixPaths.indexOf(fullPath) !== -1) {
+ return
+ }
+ // 只取重建标签所需的字段,不要把路由定义上的 component/children 也塞进 visitedViews;
+ // query/params 一律用空对象:固定标签来自右键菜单,天然不带参数,
+ // 且 RouterVo.query 是菜单的默认参数 JSON 字符串,直接透传会被 vue-router 当成非法 query
+ this.$store.dispatch('tagsView/addView', {
+ path: fullPath,
+ fullPath: fullPath,
+ name: route.name,
+ query: {},
+ params: {},
+ meta: Object.assign({}, route.meta, { affix: true, pinned: true })
+ })
+ })
+ })
+ },
+ /**
+ * 按完整路径在当前路由表中递归查找对应路由
+ *
+ * 后端只存了 path,恢复标签时需要 name/query/meta 才能正常渲染与跳转。
+ *
+ * 两个坑:
+ * 1. 路径形态不一致 —— $route.path 形如 '/iot/device'(带前导斜杠),
+ * 而 filterAsyncRouter 生成的子路由 path 形如 'device'(相对父级,不带斜杠)。
+ * 2. 子路由 path 是**相对父级**的,必须逐层拼接成完整路径才能与库里的绝对路径比较;
+ * 只比节点自身 path 的话,'device' 永远匹配不上 '/iot/device'。
+ * 因此这里按「父级完整路径 + 当前节点 path」逐层累积。
+ *
+ * 返回值刻意带上拼出来的 path:命中的是路由**节点**,它自己的 route.path 是相对父级的
+ * ('device'),调用方若拿 route.path 再归一化会得到 '/device' —— 这正是固定标签
+ * 恢复后点开 404 的成因。绝对路径只能由本方法给出,不允许调用方二次推导。
+ *
+ * @param {string} fullPath 目标完整路径,如 /iot/device
+ * @param {Array} routes 待查找的路由列表,默认取全部路由
+ * @param {string} basePath 父级完整路径,递归时内部使用
+ * @returns {{route: Object, path: string}|null} 命中的路由与其绝对路径,未命中返回 null
+ */
+ findRouteByPath(fullPath, routes, basePath) {
+ const target = normalizePath(fullPath)
+ if (!target) {
+ return null
+ }
+ const list = routes || this.routes
+ const prefix = basePath === undefined ? '' : basePath
+ for (const route of list) {
+ // route.path 已是绝对路径(以 / 开头)时直接用,否则视为相对父级拼接
+ const current = route.path.charAt(0) === '/'
+ ? normalizePath(route.path)
+ : normalizePath(prefix + '/' + route.path)
+ if (current === target) {
+ return { route, path: current }
+ }
+ if (route.children && route.children.length) {
+ const hit = this.findRouteByPath(target, route.children, current)
+ if (hit) {
+ return hit
+ }
+ }
+ }
+ return null
},
addTags() {
const { name } = this.$route
diff --git a/yack-web/src/permission.js b/yack-web/src/permission.js
index 9df13bc9..a5b4acd1 100644
--- a/yack-web/src/permission.js
+++ b/yack-web/src/permission.js
@@ -17,7 +17,10 @@ router.beforeEach((to, from, next) => {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title);
/* has token*/
if (to.path === '/login') {
- next({ path: '/bigscreen' });
+ // 已登录还访问登录页:回首页。
+ // 这里不再硬编码 /bigscreen —— 登录后的落地页由个人设置决定,
+ // 真正的跳转在 login.vue 的 resolveLoginRedirect 里完成。
+ next({ path: '/' });
NProgress.done();
} else {
if (store.getters.roles.length === 0) {
@@ -54,7 +57,12 @@ router.beforeEach((to, from, next) => {
next();
return;
}
- next(`/login?redirect=${to.fullPath}`); // 否则全部重定向到登录页
+ // 只在「用户主动访问的深层页面」上带 redirect 回跳:登录后回原位是这类场景的诉求。
+ // 首页与根路径是应用的默认入口(登出后 location.href='/index'、直接打开站点是 '/'),
+ // 它们不携带任何「用户意图」;一旦也拼进 redirect,login.vue 里
+ // 「redirect 优先于个人设置」的判定会把 sys_user_setting.login_redirect 永久顶掉。
+ const isDefaultEntry = to.path === '/' || to.path === '/index';
+ next(isDefaultEntry ? '/login' : `/login?redirect=${to.fullPath}`);
NProgress.done();
}
}
diff --git a/yack-web/src/router/index.js b/yack-web/src/router/index.js
index 5a4ab484..0f417e90 100644
--- a/yack-web/src/router/index.js
+++ b/yack-web/src/router/index.js
@@ -71,7 +71,10 @@ export const constantRoutes = [
{
path: '',
component: Layout,
- redirect: '/bigscreen',
+ // 根路径兜底到首页。系统设置里的「登录后显示页面」不能在这里生效——
+ // 路由表是静态定义的、生成时拿不到异步配置,硬拼会与 permission.js 形成跳转循环。
+ // 实际落地页由 login.vue 的 resolveLoginRedirect 决定,这里只承接「已登录用户访问 /」。
+ redirect: '/index',
children: [
{
path: 'index',
diff --git a/yack-web/src/store/modules/tagsView.js b/yack-web/src/store/modules/tagsView.js
index 9fcafe66..8a308496 100644
--- a/yack-web/src/store/modules/tagsView.js
+++ b/yack-web/src/store/modules/tagsView.js
@@ -1,4 +1,11 @@
-const PINNED_STORAGE_KEY = 'pinned-tags'
+import { Message } from 'element-ui'
+import { getPinnedTags, updatePinnedTags } from '@/api/system/userSetting'
+
+// 固定标签的持久化说明:
+// 原实现把标签写入浏览器 localStorage(key: pinned-tags),只能在本机本浏览器生效,
+// 换浏览器或换电脑就丢。现改为落库(sys_user_setting.pinned_tags),
+// 固定项按 path 存取,顺序即固定区顺序。
+// 本模块不再直接依赖 localStorage——组件初始化时从后端拉取,由 TagsView 触发。
// 只持久化「重新打开标签」所需的最小字段,避免把 meta 里的临时/函数数据写进 localStorage
function toStorableView(view) {
@@ -13,21 +20,9 @@ function toStorableView(view) {
}
}
-function loadPinnedViews() {
- try {
- const list = JSON.parse(localStorage.getItem(PINNED_STORAGE_KEY))
- return Array.isArray(list) ? list : []
- } catch (e) {
- return []
- }
-}
-
-function savePinnedViews(list) {
- try {
- localStorage.setItem(PINNED_STORAGE_KEY, JSON.stringify(list))
- } catch (e) {
- // localStorage 不可用(隐私模式/超配额)时降级为仅当前会话有效
- }
+// 落库时只提交 path 列表,其余字段可由路由表还原,避免把冗余的 title/icon 存进库
+function toPinnedPaths(list) {
+ return (list || []).map(v => v.path).filter(p => !!p)
}
// 固定区 = 排在普通标签之前的 affix 标签;新固定/取消固定的标签插到固定区末尾
@@ -40,8 +35,9 @@ const state = {
visitedViews: [],
cachedViews: [],
iframeViews: [],
- // 用户右键「固定标签到任务栏」的标签,刷新后仍然存在
- pinnedViews: loadPinnedViews()
+ // 用户右键「固定标签到任务栏」的标签,刷新/换设备后仍然存在
+ // 初始为空,由 TagsView 在拿到后端数据后通过 SET_PINNED_VIEWS 填充
+ pinnedViews: []
}
const mutations = {
@@ -125,11 +121,9 @@ const mutations = {
if (!state.pinnedViews.some(v => v.path === view.path)) {
state.pinnedViews.push(toStorableView(target))
}
- savePinnedViews(state.pinnedViews)
},
UNPIN_VIEW: (state, view) => {
state.pinnedViews = state.pinnedViews.filter(v => v.path !== view.path)
- savePinnedViews(state.pinnedViews)
const index = state.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) return
const target = state.visitedViews[index]
@@ -138,6 +132,10 @@ const mutations = {
state.visitedViews.splice(index, 1)
moveToAffixBlockEnd(state.visitedViews, target)
},
+ // 从后端载入固定标签,tagsView 初始化时调用
+ SET_PINNED_VIEWS: (state, list) => {
+ state.pinnedViews = Array.isArray(list) ? list : []
+ },
// 拖拽排序:把 visitedViews 里的 fromIndex 挪到 toIndex(toIndex 是「最终落在」的位置)
SORT_VISITED_VIEWS: (state, { fromIndex, toIndex }) => {
const list = state.visitedViews
@@ -204,6 +202,45 @@ const actions = {
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
+ /**
+ * 从后端载入该用户固定的任务栏标签
+ *
+ * 落库后固定项不再依赖 localStorage,换浏览器/换电脑登录同一账号即可恢复。
+ * 失败时保持空列表,不阻塞标签栏渲染。
+ */
+ loadPinnedViews({ commit }) {
+ return getPinnedTags()
+ .then(res => {
+ const paths = Array.isArray(res.data) ? res.data : []
+ // 只保留 path,完整 view 由 TagsView 用当前路由表补全
+ commit('SET_PINNED_VIEWS', paths.map(p => ({ path: p })))
+ return paths
+ })
+ .catch(() => {
+ commit('SET_PINNED_VIEWS', [])
+ return []
+ })
+ },
+ /**
+ * 固定/取消固定后把最新 path 列表落库
+ *
+ * 失败时只提示不打断操作:内存中的标签区已经变了,
+ * 用户不至于因为一次网络抖动失去反馈。
+ */
+ persistPinnedViews({ state }) {
+ return updatePinnedTags(toPinnedPaths(state.pinnedViews))
+ .catch(() => {
+ Message.warning('固定标签保存失败,下次登录可能不生效')
+ })
+ },
+ pinView({ commit, dispatch, state }, view) {
+ commit('PIN_VIEW', view)
+ return dispatch('persistPinnedViews')
+ },
+ unpinView({ commit, dispatch, state }, view) {
+ commit('UNPIN_VIEW', view)
+ return dispatch('persistPinnedViews')
+ },
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
@@ -282,14 +319,6 @@ const actions = {
updateVisitedView({ commit }, view) {
commit('UPDATE_VISITED_VIEW', view)
},
- // 固定标签到任务栏(标签栏最左侧固定区,刷新后仍存在)
- pinView({ commit }, view) {
- commit('PIN_VIEW', view)
- },
- // 取消固定
- unpinView({ commit }, view) {
- commit('UNPIN_VIEW', view)
- },
// 拖拽排序
sortVisitedViews({ commit }, payload) {
commit('SORT_VISITED_VIEWS', payload)
diff --git a/yack-web/src/views/login.vue b/yack-web/src/views/login.vue
index 72c51806..6cd3da95 100644
--- a/yack-web/src/views/login.vue
+++ b/yack-web/src/views/login.vue
@@ -180,6 +180,7 @@ import { getCodeImg, checkBindId, getErrorMsg, socialLogin, bindLogin, getSmsLog
import Cookies from 'js-cookie';
import { encrypt, decrypt } from '@/utils/jsencrypt';
import { setToken } from '@/utils/auth';
+import { getUserSetting } from '@/api/system/userSetting';
import langSelect from '@/layout/components/langSelect';
export default {
@@ -312,6 +313,34 @@ export default {
},
methods: {
+ /**
+ * 解析登录后的跳转目标
+ * 优先级:URL 携带 redirect(分享/未授权跳转) > 个人设置 > 默认大屏
+ * 任何分支失败都退到 /bigscreen,不阻断登录
+ *
+ * 所有登录入口(账号密码 / 短信 / 绑定 / 第三方)都必须走这里,
+ * 否则各写一份硬编码 '/bigscreen' 会让「登录后显示页面」只对其中一种登录方式生效。
+ */
+ resolveLoginRedirect() {
+ if (this.redirect) {
+ return Promise.resolve(this.redirect);
+ }
+ return getUserSetting()
+ .then((res) => {
+ const data = res.data || {};
+ return data.loginRedirect || '/bigscreen';
+ })
+ .catch(() => '/bigscreen');
+ },
+ /**
+ * 按登录后落地页跳转
+ * 独立成方法,避免各登录入口重复写 router.push 与 catch
+ */
+ redirectAfterLogin() {
+ this.resolveLoginRedirect().then((path) => {
+ this.$router.push({ path }).catch(() => {});
+ });
+ },
changesmsForm() {
this.acountLogin = !this.acountLogin;
this.phoneLogin = !this.phoneLogin;
@@ -360,11 +389,7 @@ export default {
// 保存token
this.loading = true;
setToken(res.token);
- this.$router
- .push({
- path: '/bigscreen',
- })
- .catch(() => {});
+ this.redirectAfterLogin();
if (this.captchaOnOff) {
this.getCode();
this.loading = false;
@@ -482,11 +507,7 @@ export default {
this.smsLoading = true;
smsLogin(this.smsLoginForm).then((res) => {
setToken(res.data);
- this.$router
- .push({
- path: '/bigscreen',
- })
- .catch(() => {});
+ this.redirectAfterLogin();
});
}
});
@@ -515,11 +536,7 @@ export default {
.then((res) => {
// 保存token
setToken(res.token);
- this.$router
- .push({
- path: '/bigscreen',
- })
- .catch(() => {});
+ this.redirectAfterLogin();
})
.catch(() => {
this.loading = false;
@@ -570,11 +587,8 @@ export default {
this.$store
.dispatch('Login', this.loginForm)
.then(() => {
- this.$router
- .push({
- path: '/bigscreen',
- })
- .catch(() => {});
+ // 跳转优先级:URL 携带的 redirect(如会话过期前所在页) > 个人设置 > 默认大屏
+ this.redirectAfterLogin();
})
.catch(() => {
this.loading = false;
diff --git a/yack-web/src/views/system/setting/components/SettingRow.vue b/yack-web/src/views/system/setting/components/SettingRow.vue
new file mode 100644
index 00000000..0bbaae57
--- /dev/null
+++ b/yack-web/src/views/system/setting/components/SettingRow.vue
@@ -0,0 +1,75 @@
+
+
+
{{ label }}
+
+
+
+
{{ desc }}
+
+
+
+
+
+
diff --git a/yack-web/src/views/system/setting/components/UserSettingForm.vue b/yack-web/src/views/system/setting/components/UserSettingForm.vue
new file mode 100644
index 00000000..df8c08cb
--- /dev/null
+++ b/yack-web/src/views/system/setting/components/UserSettingForm.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+ 保存
+ 重置
+
+
+
+
+
+
+
diff --git a/yack-web/src/views/system/setting/index.vue b/yack-web/src/views/system/setting/index.vue
new file mode 100644
index 00000000..773ba697
--- /dev/null
+++ b/yack-web/src/views/system/setting/index.vue
@@ -0,0 +1,60 @@
+
+
+
系统设置
+
+
+
+
+
全局配置
+
+
+
+
+ {{ readImeiIccid ? '已开启' : '已关闭' }}
+
+
+
+
+
+
+
+
+
diff --git a/yack-web/src/views/system/user/profile/index.vue b/yack-web/src/views/system/user/profile/index.vue
index 3288e0cb..30b22bab 100644
--- a/yack-web/src/views/system/user/profile/index.vue
+++ b/yack-web/src/views/system/user/profile/index.vue
@@ -18,6 +18,10 @@
修改密码
+
+
+ 个人设置
+
@@ -39,6 +43,12 @@
+
@@ -49,11 +59,12 @@ import userAvatar from './userAvatar';
import userInfo from './userInfo';
import baseInfo from './baseInfo';
import resetPwd from './resetPwd';
+import userSettingForm from '@/views/system/setting/components/UserSettingForm';
import { getUserProfile } from '@/api/system/user';
export default {
name: 'Profile',
- components: { userAvatar, userInfo, baseInfo, resetPwd },
+ components: { userAvatar, userInfo, baseInfo, resetPwd, userSettingForm },
data() {
return {
tabIndex: 0,