mirror of
				https://gitee.com/lab1024/smart-admin.git
				synced 2025-11-04 18:33:43 +08:00 
			
		
		
		
	更新V3.13.0版本:【新增】顶部菜单模式;【优化】因kaptcha有漏洞,弃用;【优化】三级等保默认值
This commit is contained in:
		@@ -29,7 +29,6 @@
 | 
			
		||||
        <google-linkedhashmap.version>1.4.2</google-linkedhashmap.version>
 | 
			
		||||
        <google-guava.version>20.0</google-guava.version>
 | 
			
		||||
        <user-agent-utils.version>1.21</user-agent-utils.version>
 | 
			
		||||
        <kaptcha.version>2.3.2</kaptcha.version>
 | 
			
		||||
        <reflections.version>0.9.11</reflections.version>
 | 
			
		||||
        <commons-io.version>2.15.0</commons-io.version>
 | 
			
		||||
        <commons-lang3.version>3.12.0</commons-lang3.version>
 | 
			
		||||
@@ -127,18 +126,6 @@
 | 
			
		||||
                <version>${user-agent-utils.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>com.github.penggle</groupId>
 | 
			
		||||
                <artifactId>kaptcha</artifactId>
 | 
			
		||||
                <version>${kaptcha.version}</version>
 | 
			
		||||
                <exclusions>
 | 
			
		||||
                    <exclusion>
 | 
			
		||||
                        <groupId>javax.servlet</groupId>
 | 
			
		||||
                        <artifactId>*</artifactId>
 | 
			
		||||
                    </exclusion>
 | 
			
		||||
                </exclusions>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.reflections</groupId>
 | 
			
		||||
                <artifactId>reflections</artifactId>
 | 
			
		||||
 
 | 
			
		||||
@@ -143,11 +143,6 @@
 | 
			
		||||
            <artifactId>guava</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.github.penggle</groupId>
 | 
			
		||||
            <artifactId>kaptcha</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.googlecode.concurrentlinkedhashmap</groupId>
 | 
			
		||||
            <artifactId>concurrentlinkedhashmap-lru</artifactId>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,22 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
 | 
			
		||||
import cn.hutool.captcha.CaptchaUtil;
 | 
			
		||||
import cn.hutool.captcha.LineCaptcha;
 | 
			
		||||
import cn.hutool.core.img.ImgUtil;
 | 
			
		||||
import cn.hutool.core.util.RandomUtil;
 | 
			
		||||
import jakarta.annotation.Resource;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import net.lab1024.sa.base.common.constant.StringConst;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.ResponseDTO;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.SystemEnvironment;
 | 
			
		||||
import net.lab1024.sa.base.common.exception.BusinessException;
 | 
			
		||||
import net.lab1024.sa.base.constant.RedisKeyConst;
 | 
			
		||||
import net.lab1024.sa.base.module.support.captcha.domain.CaptchaForm;
 | 
			
		||||
import net.lab1024.sa.base.module.support.captcha.domain.CaptchaVO;
 | 
			
		||||
import net.lab1024.sa.base.module.support.redis.RedisService;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.util.Base64Utils;
 | 
			
		||||
 | 
			
		||||
import javax.imageio.ImageIO;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +27,7 @@ import java.util.UUID;
 | 
			
		||||
 * @Date 2021/8/31 20:52
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Service
 | 
			
		||||
@@ -40,9 +38,6 @@ public class CaptchaService {
 | 
			
		||||
     */
 | 
			
		||||
    private static final long EXPIRE_SECOND = 65L;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private DefaultKaptcha defaultKaptcha;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private SystemEnvironment systemEnvironment;
 | 
			
		||||
 | 
			
		||||
@@ -52,20 +47,23 @@ public class CaptchaService {
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成图形验证码
 | 
			
		||||
     * 默认 1 分钟有效期
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public CaptchaVO generateCaptcha() {
 | 
			
		||||
        String captchaText = defaultKaptcha.createText();
 | 
			
		||||
        BufferedImage image = defaultKaptcha.createImage(captchaText);
 | 
			
		||||
 | 
			
		||||
        String base64Code;
 | 
			
		||||
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
 | 
			
		||||
            ImageIO.write(image, "jpg", os);
 | 
			
		||||
            base64Code = Base64Utils.encodeToString(os.toByteArray());
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.error("generateCaptcha error:", e);
 | 
			
		||||
            throw new BusinessException("生成验证码错误");
 | 
			
		||||
        }
 | 
			
		||||
        //生成四位验证码
 | 
			
		||||
        String captchaText = RandomUtil.randomNumbers(4);
 | 
			
		||||
 | 
			
		||||
        //定义图形验证码的长、宽、验证码位数、干扰线数量
 | 
			
		||||
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(125, 43, 4, 80);
 | 
			
		||||
 | 
			
		||||
        //设置背景颜色
 | 
			
		||||
        lineCaptcha.setBackground(new Color(230, 244, 255));
 | 
			
		||||
 | 
			
		||||
        //生成图片
 | 
			
		||||
        Image image = lineCaptcha.createImage(captchaText);
 | 
			
		||||
 | 
			
		||||
        //转为base64
 | 
			
		||||
        String base64Code = ImgUtil.toBase64(image, "jpg");
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
         * 返回验证码对象
 | 
			
		||||
@@ -88,7 +86,6 @@ public class CaptchaService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验图形验证码
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public ResponseDTO<String> checkCaptcha(CaptchaForm captchaForm) {
 | 
			
		||||
        if (StringUtils.isBlank(captchaForm.getCaptchaUuid()) || StringUtils.isBlank(captchaForm.getCaptchaCode())) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Lists;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码颜色
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
public class CaptchaColor {
 | 
			
		||||
 | 
			
		||||
    public static Color getColor() {
 | 
			
		||||
 | 
			
		||||
        List<Color> colors = Lists.newArrayList();
 | 
			
		||||
        colors.add(new Color(0, 135, 255));
 | 
			
		||||
        colors.add(new Color(51, 153, 51));
 | 
			
		||||
        colors.add(new Color(255, 102, 102));
 | 
			
		||||
        colors.add(new Color(255, 153, 0));
 | 
			
		||||
        colors.add(new Color(153, 102, 0));
 | 
			
		||||
        colors.add(new Color(153, 102, 153));
 | 
			
		||||
        colors.add(new Color(51, 153, 153));
 | 
			
		||||
        colors.add(new Color(102, 102, 255));
 | 
			
		||||
        colors.add(new Color(0, 102, 204));
 | 
			
		||||
        colors.add(new Color(204, 51, 51));
 | 
			
		||||
        colors.add(new Color(128, 153, 65));
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        int colorIndex = random.nextInt(10);
 | 
			
		||||
        return colors.get(colorIndex);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
 | 
			
		||||
import com.google.code.kaptcha.util.Config;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 | 
			
		||||
import java.util.Properties;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码配置
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
public class CaptchaConfig {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    public DefaultKaptcha getDefaultKaptcha() {
 | 
			
		||||
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
 | 
			
		||||
        Properties properties = new Properties();
 | 
			
		||||
        properties.setProperty("kaptcha.border", "no");
 | 
			
		||||
        properties.setProperty("kaptcha.border.color", "34,114,200");
 | 
			
		||||
        properties.setProperty("kaptcha.image.width", "125");
 | 
			
		||||
        properties.setProperty("kaptcha.image.height", "45");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.char.string", "123456789");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.char.length", "4");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.font.names", "Arial,Arial Narrow,Serif,Helvetica,Tahoma,Times New Roman,Verdana");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.font.size", "38");
 | 
			
		||||
 | 
			
		||||
        properties.setProperty("kaptcha.background.clear.from", "white");
 | 
			
		||||
        properties.setProperty("kaptcha.background.clear.to", "white");
 | 
			
		||||
 | 
			
		||||
        properties.setProperty("kaptcha.word.impl", CaptchaWordRenderer.class.getName());
 | 
			
		||||
        properties.setProperty("kaptcha.noise.impl", CaptchaNoise.class.getName());
 | 
			
		||||
 | 
			
		||||
        Config config = new Config(properties);
 | 
			
		||||
        defaultKaptcha.setConfig(config);
 | 
			
		||||
        return defaultKaptcha;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.NoiseProducer;
 | 
			
		||||
import com.google.code.kaptcha.util.Configurable;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码加噪处理
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
public class CaptchaNoise extends Configurable implements NoiseProducer {
 | 
			
		||||
 | 
			
		||||
    public CaptchaNoise() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void makeNoise(BufferedImage image, float factorOne, float factorTwo, float factorThree, float factorFour) {
 | 
			
		||||
 | 
			
		||||
        int width = image.getWidth();
 | 
			
		||||
        int height = image.getHeight();
 | 
			
		||||
        Graphics2D graph = (Graphics2D) image.getGraphics();
 | 
			
		||||
        graph.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
 | 
			
		||||
        graph.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        int noiseLineNum = random.nextInt(3);
 | 
			
		||||
        if (noiseLineNum == 0) {
 | 
			
		||||
            noiseLineNum = 1;
 | 
			
		||||
        }
 | 
			
		||||
        for (int i = 0; i < noiseLineNum; i++) {
 | 
			
		||||
            graph.setColor(CaptchaColor.getColor());
 | 
			
		||||
            graph.drawLine(random.nextInt(width), random.nextInt(height), 10 + random.nextInt(20), 10 + random.nextInt(20));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        graph.dispose();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.text.WordRenderer;
 | 
			
		||||
import com.google.code.kaptcha.util.Configurable;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.font.FontRenderContext;
 | 
			
		||||
import java.awt.font.GlyphVector;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码字体生成
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
public class CaptchaWordRenderer extends Configurable implements WordRenderer {
 | 
			
		||||
 | 
			
		||||
    public CaptchaWordRenderer() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public BufferedImage renderWord(String word, int width, int height) {
 | 
			
		||||
        int fontSize = this.getConfig().getTextProducerFontSize();
 | 
			
		||||
        Font[] fonts = this.getConfig().getTextProducerFonts(fontSize);
 | 
			
		||||
        int charSpace = this.getConfig().getTextProducerCharSpace();
 | 
			
		||||
        BufferedImage image = new BufferedImage(width, height, 2);
 | 
			
		||||
 | 
			
		||||
        Graphics2D g2D = image.createGraphics();
 | 
			
		||||
        g2D.setColor(Color.WHITE);
 | 
			
		||||
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
 | 
			
		||||
        hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
 | 
			
		||||
        g2D.setRenderingHints(hints);
 | 
			
		||||
        g2D.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
 | 
			
		||||
 | 
			
		||||
        FontRenderContext frc = g2D.getFontRenderContext();
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        int startPosY = (height - fontSize) / 5 + fontSize;
 | 
			
		||||
        char[] wordChars = word.toCharArray();
 | 
			
		||||
        Font[] chosenFonts = new Font[wordChars.length];
 | 
			
		||||
        int[] charWidths = new int[wordChars.length];
 | 
			
		||||
        int widthNeeded = 0;
 | 
			
		||||
 | 
			
		||||
        int startPosX;
 | 
			
		||||
        for (startPosX = 0; startPosX < wordChars.length; ++startPosX) {
 | 
			
		||||
            chosenFonts[startPosX] = fonts[random.nextInt(fonts.length)];
 | 
			
		||||
            char[] charToDraw = new char[]{wordChars[startPosX]};
 | 
			
		||||
            GlyphVector gv = chosenFonts[startPosX].createGlyphVector(frc, charToDraw);
 | 
			
		||||
            charWidths[startPosX] = (int) gv.getVisualBounds().getWidth();
 | 
			
		||||
            if (startPosX > 0) {
 | 
			
		||||
                widthNeeded += 2;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            widthNeeded += charWidths[startPosX];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        startPosX = (width - widthNeeded) / 2;
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < wordChars.length; ++i) {
 | 
			
		||||
            g2D.setColor(CaptchaColor.getColor());
 | 
			
		||||
            g2D.setFont(chosenFonts[i].deriveFont(Font.PLAIN));
 | 
			
		||||
            char[] charToDraw = new char[]{wordChars[i]};
 | 
			
		||||
            g2D.drawChars(charToDraw, 0, charToDraw.length, startPosX, startPosY);
 | 
			
		||||
            startPosX = startPosX + charWidths[i] + charSpace;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return image;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -107,7 +107,7 @@ public class Level3ProtectConfigService {
 | 
			
		||||
     * 最低活跃时间(单位:秒),超过此时间没有操作系统就会被冻结,默认-1 代表不限制,永不冻结; 默认 30分钟
 | 
			
		||||
     */
 | 
			
		||||
    public int getLoginActiveTimeoutSeconds() {
 | 
			
		||||
        return loginActiveTimeoutSeconds;
 | 
			
		||||
        return loginActiveTimeoutSeconds > 0 ? loginActiveTimeoutSeconds : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -167,6 +167,7 @@ public class Level3ProtectConfigService {
 | 
			
		||||
 | 
			
		||||
        if (configForm.getLoginActiveTimeoutMinutes() != null) {
 | 
			
		||||
            this.loginActiveTimeoutSeconds = configForm.getLoginActiveTimeoutMinutes() * 60;
 | 
			
		||||
            this.loginActiveTimeoutSeconds = loginActiveTimeoutSeconds > 0 ? loginActiveTimeoutSeconds : -1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (configForm.getPasswordComplexityEnabled() != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,6 @@
 | 
			
		||||
        <google-linkedhashmap.version>1.4.2</google-linkedhashmap.version>
 | 
			
		||||
        <google-guava.version>20.0</google-guava.version>
 | 
			
		||||
        <user-agent-utils.version>1.21</user-agent-utils.version>
 | 
			
		||||
        <kaptcha.version>2.3.2</kaptcha.version>
 | 
			
		||||
        <reflections.version>0.9.11</reflections.version>
 | 
			
		||||
        <commons-io.version>2.15.0</commons-io.version>
 | 
			
		||||
        <commons-lang3.version>3.12.0</commons-lang3.version>
 | 
			
		||||
@@ -152,12 +151,6 @@
 | 
			
		||||
                <version>${user-agent-utils.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>com.github.penggle</groupId>
 | 
			
		||||
                <artifactId>kaptcha</artifactId>
 | 
			
		||||
                <version>${kaptcha.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.reflections</groupId>
 | 
			
		||||
                <artifactId>reflections</artifactId>
 | 
			
		||||
 
 | 
			
		||||
@@ -163,11 +163,6 @@
 | 
			
		||||
            <artifactId>guava</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.github.penggle</groupId>
 | 
			
		||||
            <artifactId>kaptcha</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.googlecode.concurrentlinkedhashmap</groupId>
 | 
			
		||||
            <artifactId>concurrentlinkedhashmap-lru</artifactId>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,22 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
 | 
			
		||||
import cn.hutool.captcha.CaptchaUtil;
 | 
			
		||||
import cn.hutool.captcha.LineCaptcha;
 | 
			
		||||
import cn.hutool.core.img.ImgUtil;
 | 
			
		||||
import cn.hutool.core.util.RandomUtil;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import net.lab1024.sa.base.common.constant.StringConst;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.ResponseDTO;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.SystemEnvironment;
 | 
			
		||||
import net.lab1024.sa.base.common.exception.BusinessException;
 | 
			
		||||
import net.lab1024.sa.base.constant.RedisKeyConst;
 | 
			
		||||
import net.lab1024.sa.base.module.support.captcha.domain.CaptchaForm;
 | 
			
		||||
import net.lab1024.sa.base.module.support.captcha.domain.CaptchaVO;
 | 
			
		||||
import net.lab1024.sa.base.module.support.redis.RedisService;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.util.Base64Utils;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Resource;
 | 
			
		||||
import javax.imageio.ImageIO;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +27,7 @@ import java.util.UUID;
 | 
			
		||||
 * @Date 2021/8/31 20:52
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Service
 | 
			
		||||
@@ -40,9 +38,6 @@ public class CaptchaService {
 | 
			
		||||
     */
 | 
			
		||||
    private static final long EXPIRE_SECOND = 65L;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private DefaultKaptcha defaultKaptcha;
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private SystemEnvironment systemEnvironment;
 | 
			
		||||
 | 
			
		||||
@@ -52,20 +47,23 @@ public class CaptchaService {
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成图形验证码
 | 
			
		||||
     * 默认 1 分钟有效期
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public CaptchaVO generateCaptcha() {
 | 
			
		||||
        String captchaText = defaultKaptcha.createText();
 | 
			
		||||
        BufferedImage image = defaultKaptcha.createImage(captchaText);
 | 
			
		||||
 | 
			
		||||
        String base64Code;
 | 
			
		||||
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
 | 
			
		||||
            ImageIO.write(image, "jpg", os);
 | 
			
		||||
            base64Code = Base64Utils.encodeToString(os.toByteArray());
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.error("generateCaptcha error:", e);
 | 
			
		||||
            throw new BusinessException("生成验证码错误");
 | 
			
		||||
        }
 | 
			
		||||
        //生成四位验证码
 | 
			
		||||
        String captchaText = RandomUtil.randomNumbers(4);
 | 
			
		||||
 | 
			
		||||
        //定义图形验证码的长、宽、验证码位数、干扰线数量
 | 
			
		||||
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(125, 43, 4, 80);
 | 
			
		||||
 | 
			
		||||
        //设置背景颜色
 | 
			
		||||
        lineCaptcha.setBackground(new Color(230, 244, 255));
 | 
			
		||||
 | 
			
		||||
        //生成图片
 | 
			
		||||
        Image image = lineCaptcha.createImage(captchaText);
 | 
			
		||||
 | 
			
		||||
        //转为base64
 | 
			
		||||
        String base64Code = ImgUtil.toBase64(image, "jpg");
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
         * 返回验证码对象
 | 
			
		||||
@@ -88,7 +86,6 @@ public class CaptchaService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验图形验证码
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public ResponseDTO<String> checkCaptcha(CaptchaForm captchaForm) {
 | 
			
		||||
        if (StringUtils.isBlank(captchaForm.getCaptchaUuid()) || StringUtils.isBlank(captchaForm.getCaptchaCode())) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Lists;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码颜色
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
public class CaptchaColor {
 | 
			
		||||
 | 
			
		||||
    public static Color getColor() {
 | 
			
		||||
 | 
			
		||||
        List<Color> colors = Lists.newArrayList();
 | 
			
		||||
        colors.add(new Color(0, 135, 255));
 | 
			
		||||
        colors.add(new Color(51, 153, 51));
 | 
			
		||||
        colors.add(new Color(255, 102, 102));
 | 
			
		||||
        colors.add(new Color(255, 153, 0));
 | 
			
		||||
        colors.add(new Color(153, 102, 0));
 | 
			
		||||
        colors.add(new Color(153, 102, 153));
 | 
			
		||||
        colors.add(new Color(51, 153, 153));
 | 
			
		||||
        colors.add(new Color(102, 102, 255));
 | 
			
		||||
        colors.add(new Color(0, 102, 204));
 | 
			
		||||
        colors.add(new Color(204, 51, 51));
 | 
			
		||||
        colors.add(new Color(128, 153, 65));
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        int colorIndex = random.nextInt(10);
 | 
			
		||||
        return colors.get(colorIndex);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
 | 
			
		||||
import com.google.code.kaptcha.util.Config;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 | 
			
		||||
import java.util.Properties;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码配置
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
public class CaptchaConfig {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    public DefaultKaptcha getDefaultKaptcha() {
 | 
			
		||||
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
 | 
			
		||||
        Properties properties = new Properties();
 | 
			
		||||
        properties.setProperty("kaptcha.border", "no");
 | 
			
		||||
        properties.setProperty("kaptcha.border.color", "34,114,200");
 | 
			
		||||
        properties.setProperty("kaptcha.image.width", "125");
 | 
			
		||||
        properties.setProperty("kaptcha.image.height", "45");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.char.string", "123456789");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.char.length", "4");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.font.names", "Arial,Arial Narrow,Serif,Helvetica,Tahoma,Times New Roman,Verdana");
 | 
			
		||||
        properties.setProperty("kaptcha.textproducer.font.size", "38");
 | 
			
		||||
 | 
			
		||||
        properties.setProperty("kaptcha.background.clear.from", "white");
 | 
			
		||||
        properties.setProperty("kaptcha.background.clear.to", "white");
 | 
			
		||||
 | 
			
		||||
        properties.setProperty("kaptcha.word.impl", CaptchaWordRenderer.class.getName());
 | 
			
		||||
        properties.setProperty("kaptcha.noise.impl", CaptchaNoise.class.getName());
 | 
			
		||||
 | 
			
		||||
        Config config = new Config(properties);
 | 
			
		||||
        defaultKaptcha.setConfig(config);
 | 
			
		||||
        return defaultKaptcha;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.NoiseProducer;
 | 
			
		||||
import com.google.code.kaptcha.util.Configurable;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码加噪处理
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
public class CaptchaNoise extends Configurable implements NoiseProducer {
 | 
			
		||||
 | 
			
		||||
    public CaptchaNoise() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void makeNoise(BufferedImage image, float factorOne, float factorTwo, float factorThree, float factorFour) {
 | 
			
		||||
 | 
			
		||||
        int width = image.getWidth();
 | 
			
		||||
        int height = image.getHeight();
 | 
			
		||||
        Graphics2D graph = (Graphics2D) image.getGraphics();
 | 
			
		||||
        graph.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
 | 
			
		||||
        graph.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        int noiseLineNum = random.nextInt(3);
 | 
			
		||||
        if (noiseLineNum == 0) {
 | 
			
		||||
            noiseLineNum = 1;
 | 
			
		||||
        }
 | 
			
		||||
        for (int i = 0; i < noiseLineNum; i++) {
 | 
			
		||||
            graph.setColor(CaptchaColor.getColor());
 | 
			
		||||
            graph.drawLine(random.nextInt(width), random.nextInt(height), 10 + random.nextInt(20), 10 + random.nextInt(20));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        graph.dispose();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.captcha.config;
 | 
			
		||||
 | 
			
		||||
import com.google.code.kaptcha.text.WordRenderer;
 | 
			
		||||
import com.google.code.kaptcha.util.Configurable;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.font.FontRenderContext;
 | 
			
		||||
import java.awt.font.GlyphVector;
 | 
			
		||||
import java.awt.image.BufferedImage;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 验证码字体生成
 | 
			
		||||
 *
 | 
			
		||||
 * @Author 1024创新实验室-主任: 卓大
 | 
			
		||||
 * @Date 2021-09-02 20:21:10
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
public class CaptchaWordRenderer extends Configurable implements WordRenderer {
 | 
			
		||||
 | 
			
		||||
    public CaptchaWordRenderer() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public BufferedImage renderWord(String word, int width, int height) {
 | 
			
		||||
        int fontSize = this.getConfig().getTextProducerFontSize();
 | 
			
		||||
        Font[] fonts = this.getConfig().getTextProducerFonts(fontSize);
 | 
			
		||||
        int charSpace = this.getConfig().getTextProducerCharSpace();
 | 
			
		||||
        BufferedImage image = new BufferedImage(width, height, 2);
 | 
			
		||||
 | 
			
		||||
        Graphics2D g2D = image.createGraphics();
 | 
			
		||||
        g2D.setColor(Color.WHITE);
 | 
			
		||||
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
 | 
			
		||||
        hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
 | 
			
		||||
        g2D.setRenderingHints(hints);
 | 
			
		||||
        g2D.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
 | 
			
		||||
 | 
			
		||||
        FontRenderContext frc = g2D.getFontRenderContext();
 | 
			
		||||
        Random random = new Random();
 | 
			
		||||
        int startPosY = (height - fontSize) / 5 + fontSize;
 | 
			
		||||
        char[] wordChars = word.toCharArray();
 | 
			
		||||
        Font[] chosenFonts = new Font[wordChars.length];
 | 
			
		||||
        int[] charWidths = new int[wordChars.length];
 | 
			
		||||
        int widthNeeded = 0;
 | 
			
		||||
 | 
			
		||||
        int startPosX;
 | 
			
		||||
        for (startPosX = 0; startPosX < wordChars.length; ++startPosX) {
 | 
			
		||||
            chosenFonts[startPosX] = fonts[random.nextInt(fonts.length)];
 | 
			
		||||
            char[] charToDraw = new char[]{wordChars[startPosX]};
 | 
			
		||||
            GlyphVector gv = chosenFonts[startPosX].createGlyphVector(frc, charToDraw);
 | 
			
		||||
            charWidths[startPosX] = (int) gv.getVisualBounds().getWidth();
 | 
			
		||||
            if (startPosX > 0) {
 | 
			
		||||
                widthNeeded += 2;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            widthNeeded += charWidths[startPosX];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        startPosX = (width - widthNeeded) / 2;
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < wordChars.length; ++i) {
 | 
			
		||||
            g2D.setColor(CaptchaColor.getColor());
 | 
			
		||||
            g2D.setFont(chosenFonts[i].deriveFont(Font.PLAIN));
 | 
			
		||||
            char[] charToDraw = new char[]{wordChars[i]};
 | 
			
		||||
            g2D.drawChars(charToDraw, 0, charToDraw.length, startPosX, startPosY);
 | 
			
		||||
            startPosX = startPosX + charWidths[i] + charSpace;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return image;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -108,7 +108,7 @@ public class Level3ProtectConfigService {
 | 
			
		||||
     * 最低活跃时间(单位:秒),超过此时间没有操作系统就会被冻结,默认-1 代表不限制,永不冻结; 默认 30分钟
 | 
			
		||||
     */
 | 
			
		||||
    public int getLoginActiveTimeoutSeconds() {
 | 
			
		||||
        return loginActiveTimeoutSeconds;
 | 
			
		||||
        return loginActiveTimeoutSeconds > 0 ? loginActiveTimeoutSeconds : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -168,6 +168,7 @@ public class Level3ProtectConfigService {
 | 
			
		||||
 | 
			
		||||
        if (configForm.getLoginActiveTimeoutMinutes() != null) {
 | 
			
		||||
            this.loginActiveTimeoutSeconds = configForm.getLoginActiveTimeoutMinutes() * 60;
 | 
			
		||||
            this.loginActiveTimeoutSeconds = loginActiveTimeoutSeconds > 0 ? loginActiveTimeoutSeconds : -1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (configForm.getPasswordComplexityEnabled() != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -81,13 +81,14 @@
 | 
			
		||||
    if (!value) {
 | 
			
		||||
      emit('update:value', []);
 | 
			
		||||
      emit('change', []);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (Array.isArray(value)) {
 | 
			
		||||
      emit('update:value', value);
 | 
			
		||||
      emit('change', value);
 | 
			
		||||
    } else {
 | 
			
		||||
      emit('update:value', [value]);
 | 
			
		||||
      emit('change', [value]);
 | 
			
		||||
      emit('update:value', value);
 | 
			
		||||
      emit('change', value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,10 @@ export const LAYOUT_ENUM = {
 | 
			
		||||
    value: 'top',
 | 
			
		||||
    desc: '顶部',
 | 
			
		||||
  },
 | 
			
		||||
  TOP_EXPAND: {
 | 
			
		||||
    value: 'top-expand',
 | 
			
		||||
    desc: '分组',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const PAGE_TAG_ENUM = {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <a-drawer :title="$t('setting.title')" placement="right" :open="visible" @close="close">
 | 
			
		||||
  <a-drawer :title="$t('setting.title')" placement="right" :width="410" :open="visible" @close="close">
 | 
			
		||||
    <a-form layout="horizontal" :label-col="{ span: 8 }">
 | 
			
		||||
      <a-form-item label="语言/Language">
 | 
			
		||||
        <a-select v-model:value="formState.language" @change="changeLanguage" style="width: 120px">
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 展开菜单
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="menu-container">
 | 
			
		||||
    <!-- 顶部导航:取自菜单管理中第一级菜单,建议一级菜单都设为目录 -->
 | 
			
		||||
    <TopMenu ref="topMenuRef" :collapsed="collapsed" class="top-menu" />
 | 
			
		||||
    <!-- 左侧导航:通过在一级菜单下建立页面或目录实现菜单分组 -->
 | 
			
		||||
    <RecursionMenu :collapsed="collapsed" ref="recursionMenuRef" class="recursion-menu" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
import { onMounted, ref, watch, computed } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import RecursionMenu from './recursion-menu.vue';
 | 
			
		||||
import TopMenu from './top-menu.vue';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  placeholder: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: '请选择',
 | 
			
		||||
  },
 | 
			
		||||
  collapsed: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    required: false,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 选中的顶级菜单
 | 
			
		||||
const topMenuRef = ref();
 | 
			
		||||
// 二级菜单引用
 | 
			
		||||
const recursionMenuRef = ref();
 | 
			
		||||
 | 
			
		||||
let currentRoute = useRoute();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 根据路由更新菜单展开和选中状态
 | 
			
		||||
function updateSelectKeyAndOpenKey() {
 | 
			
		||||
  // 第一步,根据路由 更新选中 顶级菜单
 | 
			
		||||
  let parentList = useUserStore().menuParentIdListMap.get(currentRoute.name) || [];
 | 
			
		||||
  console.log('parentList', parentList)
 | 
			
		||||
  if (parentList.length === 0) {
 | 
			
		||||
    topMenuRef.value.updateSelectKey(currentRoute.name);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  topMenuRef.value.updateSelectKey(parentList[0].name);
 | 
			
		||||
 | 
			
		||||
  //第二步,根据路由 更新 二级菜单的selectKey和openKey
 | 
			
		||||
  recursionMenuRef.value.updateSelectKeyAndOpenKey(parentList, currentRoute.name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(updateSelectKeyAndOpenKey);
 | 
			
		||||
 | 
			
		||||
//监听路由的变化,进行更新菜单展开项目
 | 
			
		||||
watch(currentRoute, () => {
 | 
			
		||||
  updateSelectKeyAndOpenKey();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
.menu-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,168 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 递归菜单
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="recursion-container" v-show="topMenu.children && topMenu.children.length > 0">
 | 
			
		||||
    <!-- 顶部logo区域 -->
 | 
			
		||||
    <div class="logo" @click="onGoHome" :style="sideMenuWidth" v-if="!collapsed">
 | 
			
		||||
      <img class="logo-img" :src="logoImg" />
 | 
			
		||||
      <div class="title smart-logo title-light" v-if="isLight">{{ websiteName }}</div>
 | 
			
		||||
      <div class="title smart-logo title-dark" v-if="!isLight">{{ websiteName }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="min-logo" @click="onGoHome" v-if="collapsed">
 | 
			
		||||
      <img class="logo-img" :src="logoImg" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 次级菜单展示 -->
 | 
			
		||||
    <a-menu :selectedKeys="selectedKeys" :theme="theme" :openKeys="openKeys" mode="inline">
 | 
			
		||||
      <template v-for="item in topMenu.children" :key="item.menuId">
 | 
			
		||||
        <template v-if="item.visibleFlag">
 | 
			
		||||
          <template v-if="$lodash.isEmpty(item.children)">
 | 
			
		||||
            <a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
 | 
			
		||||
              <template #icon v-if="item.icon">
 | 
			
		||||
                <component :is="$antIcons[item.icon]" />
 | 
			
		||||
              </template>
 | 
			
		||||
              {{ item.menuName }}
 | 
			
		||||
            </a-menu-item>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-else>
 | 
			
		||||
            <SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
 | 
			
		||||
          </template>
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </a-menu>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, computed } from 'vue';
 | 
			
		||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
 | 
			
		||||
import SubMenu from './sub-menu.vue';
 | 
			
		||||
import { router } from '/@/router';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import menuEmitter from './top-expand-menu-mitt';
 | 
			
		||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
import logoImg from '/@/assets/images/logo/smart-admin-logo.png';
 | 
			
		||||
 | 
			
		||||
const websiteName = computed(() => useAppConfigStore().websiteName);
 | 
			
		||||
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  collapsed: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 选中的顶级菜单
 | 
			
		||||
let topMenu = ref({});
 | 
			
		||||
menuEmitter.on('selectTopMenu', onSelectTopMenu);
 | 
			
		||||
 | 
			
		||||
// 监听选中顶级菜单事件
 | 
			
		||||
function onSelectTopMenu(selectedTopMenu) {
 | 
			
		||||
  topMenu.value = selectedTopMenu;
 | 
			
		||||
  if (selectedTopMenu.children && selectedTopMenu.children.length > 0) {
 | 
			
		||||
    openKeys.value = _.map(selectedTopMenu.children, 'menuId').map((e) => e.toString());
 | 
			
		||||
  } else {
 | 
			
		||||
    openKeys.value = [];
 | 
			
		||||
  }
 | 
			
		||||
  selectedKeys.value = [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//展开的菜单
 | 
			
		||||
const selectedKeys = ref([]);
 | 
			
		||||
const openKeys = ref([]);
 | 
			
		||||
 | 
			
		||||
function updateSelectKeyAndOpenKey(parentList, currentSelectKey) {
 | 
			
		||||
  if (!parentList) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  //获取需要展开的menu key集合
 | 
			
		||||
  openKeys.value = _.map(parentList, 'name');
 | 
			
		||||
  selectedKeys.value = [currentSelectKey];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 页面跳转
 | 
			
		||||
function turnToPage(route) {
 | 
			
		||||
  useUserStore().deleteKeepAliveIncludes(route.menuId.toString());
 | 
			
		||||
  router.push({ name: route.menuId.toString() });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onGoHome() {
 | 
			
		||||
  router.push({ name: HOME_PAGE_NAME });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ updateSelectKeyAndOpenKey });
 | 
			
		||||
 | 
			
		||||
const isLight = computed(() => useAppConfigStore().$state.sideMenuTheme === 'light');
 | 
			
		||||
const color = computed(() => {
 | 
			
		||||
  let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
 | 
			
		||||
  return {
 | 
			
		||||
    background: isLight ? '#FFFFFF' : '#001529',
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
.recursion-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background-color: v-bind('color.background');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  .min-logo {
 | 
			
		||||
    height: @header-user-height;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
    padding: 0px 15px 0px 15px;
 | 
			
		||||
    // background-color: v-bind('color.background');
 | 
			
		||||
 | 
			
		||||
    width: 80px;
 | 
			
		||||
    z-index: 21;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    .logo-img {
 | 
			
		||||
      width: 30px;
 | 
			
		||||
      height: 30px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
.top-menu {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  color: #515a6e;
 | 
			
		||||
  border-bottom: 1px solid #f3f3f3;
 | 
			
		||||
  border-right: 1px solid #f3f3f3;
 | 
			
		||||
}
 | 
			
		||||
.logo {
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  line-height: @header-user-height;
 | 
			
		||||
  padding: 0px 15px 0px 15px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  .logo-img {
 | 
			
		||||
    width: 30px;
 | 
			
		||||
    height: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .title {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    color: v-bind('theme === "light" ? "#001529": "#ffffff"');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 第二列菜单区域
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <a-sub-menu :key="props.menuInfo.menuId.toString()">
 | 
			
		||||
    <template #icon>
 | 
			
		||||
      <component :is="$antIcons[props.menuInfo.icon]" />
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #title>{{ props.menuInfo.menuName }}</template>
 | 
			
		||||
    <template v-for="item in props.menuInfo.children" :key="item.menuId">
 | 
			
		||||
      <template v-if="item.visibleFlag">
 | 
			
		||||
        <template v-if="!item.children">
 | 
			
		||||
          <a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
 | 
			
		||||
            <template #icon>
 | 
			
		||||
              <component :is="$antIcons[item.icon]" />
 | 
			
		||||
            </template>
 | 
			
		||||
            {{ item.menuName }}
 | 
			
		||||
          </a-menu-item>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-else>
 | 
			
		||||
          <SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </template>
 | 
			
		||||
  </a-sub-menu>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  let props = defineProps({
 | 
			
		||||
    menuInfo: Object,
 | 
			
		||||
  });
 | 
			
		||||
  const emits = defineEmits(['turnToPage']);
 | 
			
		||||
  const turnToPage = (route) => {
 | 
			
		||||
    emits('turnToPage', route);
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
  ::v-deep(.ant-menu-item-selected) {
 | 
			
		||||
    border-right: 3px !important;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 展开菜单 event bus
 | 
			
		||||
 *
 | 
			
		||||
 * @Author:    1024创新实验室-主任:卓大
 | 
			
		||||
 * @Date:      2022-07-12 23:32:48
 | 
			
		||||
 * @Wechat:    zhuda1024
 | 
			
		||||
 * @Email:     lab1024@163.com
 | 
			
		||||
 * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012
 | 
			
		||||
 */
 | 
			
		||||
import mitt from 'mitt';
 | 
			
		||||
export default mitt();
 | 
			
		||||
@@ -0,0 +1,147 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 第一列菜单
 | 
			
		||||
  *
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12
 | 
			
		||||
  * @Wechat:    zhuda1024
 | 
			
		||||
  * @Email:     lab1024@163.com
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="top-menu-container">
 | 
			
		||||
    <!-- 一级菜单展示 -->
 | 
			
		||||
    <a-menu :selectedKeys="selectedKeys" mode="horizontal" :theme="theme">
 | 
			
		||||
      <template v-for="item in menuTree" :key="item.menuId">
 | 
			
		||||
        <template v-if="item.visibleFlag">
 | 
			
		||||
          <a-menu-item :key="item.menuId.toString()" @click="onSelectMenu(item)">
 | 
			
		||||
            <template #icon>
 | 
			
		||||
              <component :is="$antIcons[item.icon]" />
 | 
			
		||||
            </template>
 | 
			
		||||
            {{ item.menuName }}
 | 
			
		||||
          </a-menu-item>
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </a-menu>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
 | 
			
		||||
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
 | 
			
		||||
import { router } from '/@/router';
 | 
			
		||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
 | 
			
		||||
import menuEmitter from './top-expand-menu-mitt';
 | 
			
		||||
 | 
			
		||||
const websiteName = computed(() => useAppConfigStore().websiteName);
 | 
			
		||||
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
const menuTree = computed(() => useUserStore().getMenuTree || []);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  collapsed: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
// 展开菜单的顶级目录名字适配,只展示两个字为好
 | 
			
		||||
function menuNameAdapter(name) {
 | 
			
		||||
  return name.substr(0, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选中的顶级菜单
 | 
			
		||||
const selectedKeys = ref([]);
 | 
			
		||||
 | 
			
		||||
// 选中菜单,页面跳转
 | 
			
		||||
function onSelectMenu(menuItem) {
 | 
			
		||||
  selectedKeys.value = [menuItem.menuId.toString()];
 | 
			
		||||
  if (menuItem.menuType === MENU_TYPE_ENUM.MENU.value && (_.isEmpty(menuItem.children) || menuItem.children.every((e) => !e.visibleFlag))) {
 | 
			
		||||
    useUserStore().deleteKeepAliveIncludes(menuItem.menuId.toString());
 | 
			
		||||
    router.push({ name: menuItem.menuId.toString() });
 | 
			
		||||
  }
 | 
			
		||||
  menuEmitter.emit('selectTopMenu', menuItem);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新选中的菜单
 | 
			
		||||
function updateSelectKey(key) {
 | 
			
		||||
  selectedKeys.value = [key];
 | 
			
		||||
  let selectMenu = _.find(menuTree.value, { menuId: Number(key) });
 | 
			
		||||
  if (selectMenu) {
 | 
			
		||||
    menuEmitter.emit('selectTopMenu', selectMenu);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ updateSelectKey });
 | 
			
		||||
 | 
			
		||||
// 动态计算当前导航宽度
 | 
			
		||||
let menuInfo = computed(() => {
 | 
			
		||||
  let width = '100vw';
 | 
			
		||||
  let right = '-100vw';
 | 
			
		||||
  let selectedItem = _.find(menuTree.value, { menuId: Number(selectedKeys.value[0]) });
 | 
			
		||||
  const hasSecoundMenu = selectedItem && !_.isEmpty(selectedItem.children) && selectedItem.children.some((e) => e.visibleFlag);
 | 
			
		||||
  if (hasSecoundMenu) {
 | 
			
		||||
    if (props.collapsed) {
 | 
			
		||||
      width = 'calc(100vw - 80px)';
 | 
			
		||||
      right = 'calc(-100vw + 80px)';
 | 
			
		||||
    } else {
 | 
			
		||||
      width = 'calc(100vw - 180px)';
 | 
			
		||||
      right = 'calc(-100vw + 180px)';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    width,
 | 
			
		||||
    right,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
.top-menu {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  transition: all 0.2s, background 0s;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: v-bind('menuInfo.right');
 | 
			
		||||
  width: v-bind('menuInfo.width');
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
.ant-menu-dark {
 | 
			
		||||
  background: #1677ff;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.ant-menu-light {
 | 
			
		||||
  background: #1677ff;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
::v-deep(.ant-menu-item-selected) {
 | 
			
		||||
  background: #0958d9 !important;
 | 
			
		||||
  color: #fff !important;
 | 
			
		||||
}
 | 
			
		||||
.top-menu-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
.logo {
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  line-height: @header-user-height;
 | 
			
		||||
  padding: 0px 15px 0px 15px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  .logo-img {
 | 
			
		||||
    width: 30px;
 | 
			
		||||
    height: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .title {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    color: v-bind('theme === "light" ? "#001529": "#ffffff"');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -16,11 +16,14 @@
 | 
			
		||||
  <TopLayout v-if="layout === LAYOUT_ENUM.TOP.value" />
 | 
			
		||||
  <!--定期修改密码-->
 | 
			
		||||
  <RegularChangePasswordModal />
 | 
			
		||||
  <!--顶部展开 模式-->
 | 
			
		||||
  <TopExpandLayout v-if="layout === LAYOUT_ENUM.TOP_EXPAND.value" />
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  import { computed } from 'vue';
 | 
			
		||||
  import { LAYOUT_ENUM } from '/@/constants/layout-const';
 | 
			
		||||
  import SideExpandLayout from './side-expand-layout.vue';
 | 
			
		||||
  import TopExpandLayout from './top-expand-layout.vue';
 | 
			
		||||
  import SideLayout from './side-layout.vue';
 | 
			
		||||
  import TopLayout from './top-layout.vue';
 | 
			
		||||
  import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										279
									
								
								smart-admin-web-javascript/src/layout/top-expand-layout.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								smart-admin-web-javascript/src/layout/top-expand-layout.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
<!--
 | 
			
		||||
  *  展开菜单模式
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:40:16 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <a-layout class="admin-layout" style="min-height: 100%">
 | 
			
		||||
    <!-- 侧边菜单 side-menu -->
 | 
			
		||||
    <a-layout-sider :theme="theme" class="side-menu" :collapsed="collapsed" :trigger="null">
 | 
			
		||||
      <!-- 左侧菜单 -->
 | 
			
		||||
      <TopExpandMenu :collapsed="collapsed" />
 | 
			
		||||
    </a-layout-sider>
 | 
			
		||||
 | 
			
		||||
    <!--中间内容,一共三部分:1、顶部;2、中间内容区域;3、底部(一般是公司版权信息);-->
 | 
			
		||||
    <a-layout class="admin-layout-main" :style="`height: ${windowHeight}px`" id="smartAdminMain">
 | 
			
		||||
      <!-- 顶部头部信息 -->
 | 
			
		||||
      <a-layout-header class="smart-layout-header">
 | 
			
		||||
        <a-row justify="space-between" class="smart-layout-header-user">
 | 
			
		||||
          <a-col class="smart-layout-header-left">
 | 
			
		||||
            <span class="collapsed-button">
 | 
			
		||||
              <menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
 | 
			
		||||
              <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
 | 
			
		||||
            </span>
 | 
			
		||||
            <a-tooltip placement="bottom">
 | 
			
		||||
              <template #title>首页</template>
 | 
			
		||||
              <span class="home-button" @click="goHome">
 | 
			
		||||
                <home-outlined class="trigger" />
 | 
			
		||||
              </span>
 | 
			
		||||
            </a-tooltip>
 | 
			
		||||
            <span class="location-breadcrumb">
 | 
			
		||||
              <MenuLocationBreadcrumb />
 | 
			
		||||
            </span>
 | 
			
		||||
          </a-col>
 | 
			
		||||
          <!---用戶操作区域-->
 | 
			
		||||
          <a-col class="smart-layout-header-right">
 | 
			
		||||
            <HeaderUserSpace />
 | 
			
		||||
          </a-col>
 | 
			
		||||
        </a-row>
 | 
			
		||||
        <PageTag />
 | 
			
		||||
      </a-layout-header>
 | 
			
		||||
 | 
			
		||||
      <!--中间内容-->
 | 
			
		||||
      <a-layout-content class="admin-layout-content" id="smartAdminLayoutContent">
 | 
			
		||||
        <!--不keepAlive的iframe使用单个iframe组件-->
 | 
			
		||||
        <IframeIndex v-show="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
 | 
			
		||||
        <!--keepAlive的iframe 每个页面一个iframe组件-->
 | 
			
		||||
        <IframeIndex
 | 
			
		||||
          v-for="item in keepAliveIframePages"
 | 
			
		||||
          v-show="route.name == item.name"
 | 
			
		||||
          :key="item.name"
 | 
			
		||||
          :name="item.name"
 | 
			
		||||
          :url="item.meta.frameUrl"
 | 
			
		||||
        />
 | 
			
		||||
        <!--非iframe使用router-view-->
 | 
			
		||||
        <div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)">
 | 
			
		||||
          <router-view v-slot="{ Component }">
 | 
			
		||||
            <keep-alive :include="keepAliveIncludes">
 | 
			
		||||
              <component :is="Component" :key="route.name" />
 | 
			
		||||
            </keep-alive>
 | 
			
		||||
          </router-view>
 | 
			
		||||
        </div>
 | 
			
		||||
      </a-layout-content>
 | 
			
		||||
      <!-- footer 版权公司信息 -->
 | 
			
		||||
      <a-layout-footer class="smart-layout-footer" v-show="footerFlag"> <SmartFooter /></a-layout-footer>
 | 
			
		||||
      <!---- 回到顶部 --->
 | 
			
		||||
      <a-back-top :target="backTopTarget" :visibilityHeight="80" />
 | 
			
		||||
    </a-layout>
 | 
			
		||||
    <!-- 右侧帮助文档 help-doc -->
 | 
			
		||||
    <a-layout-sider v-show="helpDocFlag" theme="light" :width="180" class="help-doc-sider" :trigger="null" style="min-height: 100%">
 | 
			
		||||
      <SideHelpDoc />
 | 
			
		||||
    </a-layout-sider>
 | 
			
		||||
  </a-layout>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed, onMounted, ref, watch } from 'vue';
 | 
			
		||||
import HeaderUserSpace from './components/header-user-space/index.vue';
 | 
			
		||||
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
 | 
			
		||||
import PageTag from './components/page-tag/index.vue';
 | 
			
		||||
import TopExpandMenu from './components/top-expand-menu/index.vue';
 | 
			
		||||
import SmartFooter from './components/smart-footer/index.vue';
 | 
			
		||||
import { smartKeepAlive } from './components/smart-keep-alive';
 | 
			
		||||
import IframeIndex from '/@/components/framework/iframe/iframe-index.vue';
 | 
			
		||||
import watermark from '../lib/smart-watermark';
 | 
			
		||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
import SideHelpDoc from './components/side-help-doc/index.vue';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
 | 
			
		||||
 | 
			
		||||
const windowHeight = ref(window.innerHeight);
 | 
			
		||||
 | 
			
		||||
//主题颜色
 | 
			
		||||
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
//是否显示标签页
 | 
			
		||||
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
 | 
			
		||||
// 是否显示帮助文档
 | 
			
		||||
const helpDocFlag = computed(() => useAppConfigStore().$state.helpDocExpandFlag);
 | 
			
		||||
// 是否显示页脚
 | 
			
		||||
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
 | 
			
		||||
// 是否显示水印
 | 
			
		||||
const watermarkFlag = computed(() => useAppConfigStore().$state.watermarkFlag);
 | 
			
		||||
// 多余高度
 | 
			
		||||
const dueHeight = computed(() => {
 | 
			
		||||
  let due = 40;
 | 
			
		||||
  if (useAppConfigStore().$state.pageTagFlag) {
 | 
			
		||||
    due = due + 40;
 | 
			
		||||
  }
 | 
			
		||||
  if (useAppConfigStore().$state.footerFlag) {
 | 
			
		||||
    due = due + 40;
 | 
			
		||||
  }
 | 
			
		||||
  return due;
 | 
			
		||||
});
 | 
			
		||||
//是否隐藏菜单
 | 
			
		||||
const collapsed = ref(false);
 | 
			
		||||
 | 
			
		||||
//页面初始化的时候加载水印
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (watermarkFlag.value) {
 | 
			
		||||
    watermark.set('smartAdminLayoutContent', useUserStore().actualName);
 | 
			
		||||
  } else {
 | 
			
		||||
    watermark.clear();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => watermarkFlag.value,
 | 
			
		||||
  (newValue) => {
 | 
			
		||||
    if (newValue) {
 | 
			
		||||
      watermark.set('smartAdminLayoutContent', useUserStore().actualName);
 | 
			
		||||
    } else {
 | 
			
		||||
      watermark.clear();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
window.addEventListener('resize', function () {
 | 
			
		||||
  windowHeight.value = window.innerHeight;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//回到顶部
 | 
			
		||||
const backTopTarget = () => {
 | 
			
		||||
  return document.getElementById('smartAdminMain');
 | 
			
		||||
};
 | 
			
		||||
// ----------------------- keep-alive相关 -----------------------
 | 
			
		||||
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
function goHome() {
 | 
			
		||||
  router.push({ name: HOME_PAGE_NAME });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
:deep(.ant-layout-header) {
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
:deep(.layout-header) {
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header {
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  z-index: 21;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header-user {
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  border-bottom: 1px solid #f6f6f6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header-left {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
 | 
			
		||||
  .collapsed-button {
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-button {
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 0 5px;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-button:hover {
 | 
			
		||||
    background-color: #efefef;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .location-breadcrumb {
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header-right {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.admin-layout {
 | 
			
		||||
  .side-menu {
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    flex: 0 !important;
 | 
			
		||||
    min-width: inherit !important;
 | 
			
		||||
    max-width: none !important;
 | 
			
		||||
    // width: auto !important;
 | 
			
		||||
    &.fixed-side {
 | 
			
		||||
      position: fixed;
 | 
			
		||||
      height: 100vh;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      top: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .side-menu::-webkit-scrollbar {
 | 
			
		||||
    width: 4px;
 | 
			
		||||
  }
 | 
			
		||||
  .side-menu::-webkit-scrollbar-thumb {
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.2);
 | 
			
		||||
  }
 | 
			
		||||
  .side-menu::-webkit-scrollbar-track {
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .help-doc-sider {
 | 
			
		||||
    flex: 0 !important;
 | 
			
		||||
    min-width: 100px;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
    width: auto !important;
 | 
			
		||||
    &.fixed-side {
 | 
			
		||||
      position: fixed;
 | 
			
		||||
      height: 100vh;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      top: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .virtual-side {
 | 
			
		||||
    transition: all 0.2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .virtual-header {
 | 
			
		||||
    transition: all 0.2s;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
 | 
			
		||||
    &.fixed-tabs.multi-page:not(.fixed-header) {
 | 
			
		||||
      height: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .admin-layout-main {
 | 
			
		||||
    padding-top: 46px;
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .admin-layout-content {
 | 
			
		||||
    background-color: inherit;
 | 
			
		||||
    min-height: auto;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    padding: 10px 10px 0px 10px;
 | 
			
		||||
    height: calc(100% - v-bind(dueHeight) px);
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-footer {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -81,13 +81,14 @@
 | 
			
		||||
    if (!value) {
 | 
			
		||||
      emit('update:value', []);
 | 
			
		||||
      emit('change', []);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (Array.isArray(value)) {
 | 
			
		||||
      emit('update:value', value);
 | 
			
		||||
      emit('change', value);
 | 
			
		||||
    } else {
 | 
			
		||||
      emit('update:value', [value]);
 | 
			
		||||
      emit('change', [value]);
 | 
			
		||||
      emit('update:value', value);
 | 
			
		||||
      emit('change', value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,10 @@ export const LAYOUT_ENUM: SmartEnum<string> = {
 | 
			
		||||
    value: 'top',
 | 
			
		||||
    desc: '顶部',
 | 
			
		||||
  },
 | 
			
		||||
  TOP_EXPAND: {
 | 
			
		||||
    value: 'top-expand',
 | 
			
		||||
    desc: '分组',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const PAGE_TAG_ENUM: SmartEnum<string> = {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 展开菜单
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="menu-container">
 | 
			
		||||
    <!-- 顶部导航:取自菜单管理中第一级菜单,建议一级菜单都设为目录 -->
 | 
			
		||||
    <TopMenu ref="topMenuRef" class="top-menu" />
 | 
			
		||||
    <!-- 左侧导航:通过在一级菜单下建立页面或目录实现菜单分组 -->
 | 
			
		||||
    <RecursionMenu :collapsed="collapsed" ref="recursionMenuRef" class="recursion-menu" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { onMounted, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import RecursionMenu from './recursion-menu.vue';
 | 
			
		||||
import TopMenu from './top-menu.vue';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  placeholder: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: '请选择',
 | 
			
		||||
  },
 | 
			
		||||
  collapsed: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    required: false,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
// 选中的顶级菜单
 | 
			
		||||
const topMenuRef = ref();
 | 
			
		||||
// 二级菜单引用
 | 
			
		||||
const recursionMenuRef = ref();
 | 
			
		||||
 | 
			
		||||
let currentRoute = useRoute();
 | 
			
		||||
 | 
			
		||||
// 根据路由更新菜单展开和选中状态
 | 
			
		||||
function updateSelectKeyAndOpenKey() {
 | 
			
		||||
  // 第一步,根据路由 更新选中 顶级菜单
 | 
			
		||||
  let parentList = useUserStore().menuParentIdListMap.get(currentRoute.name) || [];
 | 
			
		||||
  if (parentList.length === 0) {
 | 
			
		||||
    topMenuRef.value.updateSelectKey(currentRoute.name);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  topMenuRef.value.updateSelectKey(parentList[0].name);
 | 
			
		||||
 | 
			
		||||
  //第二步,根据路由 更新 二级菜单的selectKey和openKey
 | 
			
		||||
  recursionMenuRef.value.updateSelectKeyAndOpenKey(parentList, currentRoute.name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(updateSelectKeyAndOpenKey);
 | 
			
		||||
 | 
			
		||||
//监听路由的变化,进行更新菜单展开项目
 | 
			
		||||
watch(currentRoute, () => {
 | 
			
		||||
  updateSelectKeyAndOpenKey();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
.menu-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,168 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 递归菜单
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="recursion-container" v-show="topMenu.children && topMenu.children.length > 0">
 | 
			
		||||
    <!-- 顶部logo区域 -->
 | 
			
		||||
    <div class="logo" @click="onGoHome" :style="sideMenuWidth" v-if="!collapsed">
 | 
			
		||||
      <img class="logo-img" :src="logoImg" />
 | 
			
		||||
      <div class="title smart-logo title-light" v-if="isLight">{{ websiteName }}</div>
 | 
			
		||||
      <div class="title smart-logo title-dark" v-if="!isLight">{{ websiteName }}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="min-logo" @click="onGoHome" v-if="collapsed">
 | 
			
		||||
      <img class="logo-img" :src="logoImg" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 次级菜单展示 -->
 | 
			
		||||
    <a-menu :selectedKeys="selectedKeys" :theme="theme" :openKeys="openKeys" mode="inline">
 | 
			
		||||
      <template v-for="item in topMenu.children" :key="item.menuId">
 | 
			
		||||
        <template v-if="item.visibleFlag">
 | 
			
		||||
          <template v-if="$lodash.isEmpty(item.children)">
 | 
			
		||||
            <a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
 | 
			
		||||
              <template #icon v-if="item.icon">
 | 
			
		||||
                <component :is="$antIcons[item.icon]" />
 | 
			
		||||
              </template>
 | 
			
		||||
              {{ item.menuName }}
 | 
			
		||||
            </a-menu-item>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-else>
 | 
			
		||||
            <SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
 | 
			
		||||
          </template>
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </a-menu>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed } from 'vue';
 | 
			
		||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
 | 
			
		||||
import SubMenu from './sub-menu.vue';
 | 
			
		||||
import { router } from '/@/router';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import menuEmitter from './top-expand-menu-mitt';
 | 
			
		||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
import logoImg from '/@/assets/images/logo/smart-admin-logo.png';
 | 
			
		||||
 | 
			
		||||
const websiteName = computed(() => useAppConfigStore().websiteName);
 | 
			
		||||
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  collapsed: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 选中的顶级菜单
 | 
			
		||||
let topMenu = ref({});
 | 
			
		||||
menuEmitter.on('selectTopMenu', onSelectTopMenu);
 | 
			
		||||
 | 
			
		||||
// 监听选中顶级菜单事件
 | 
			
		||||
function onSelectTopMenu(selectedTopMenu) {
 | 
			
		||||
  topMenu.value = selectedTopMenu;
 | 
			
		||||
  if (selectedTopMenu.children && selectedTopMenu.children.length > 0) {
 | 
			
		||||
    openKeys.value = _.map(selectedTopMenu.children, 'menuId').map((e) => e.toString());
 | 
			
		||||
  } else {
 | 
			
		||||
    openKeys.value = [];
 | 
			
		||||
  }
 | 
			
		||||
  selectedKeys.value = [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//展开的菜单
 | 
			
		||||
const selectedKeys = ref([]);
 | 
			
		||||
const openKeys = ref([]);
 | 
			
		||||
 | 
			
		||||
function updateSelectKeyAndOpenKey(parentList, currentSelectKey) {
 | 
			
		||||
  if (!parentList) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  //获取需要展开的menu key集合
 | 
			
		||||
  openKeys.value = _.map(parentList, 'name');
 | 
			
		||||
  selectedKeys.value = [currentSelectKey];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 页面跳转
 | 
			
		||||
function turnToPage(route) {
 | 
			
		||||
  useUserStore().deleteKeepAliveIncludes(route.menuId.toString());
 | 
			
		||||
  router.push({ name: route.menuId.toString() });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onGoHome() {
 | 
			
		||||
  router.push({ name: HOME_PAGE_NAME });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ updateSelectKeyAndOpenKey });
 | 
			
		||||
 | 
			
		||||
const isLight = computed(() => useAppConfigStore().$state.sideMenuTheme === 'light');
 | 
			
		||||
const color = computed(() => {
 | 
			
		||||
  let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
 | 
			
		||||
  return {
 | 
			
		||||
    background: isLight ? '#FFFFFF' : '#001529',
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
.recursion-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background-color: v-bind('color.background');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  .min-logo {
 | 
			
		||||
    height: @header-user-height;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
    padding: 0px 15px 0px 15px;
 | 
			
		||||
    // background-color: v-bind('color.background');
 | 
			
		||||
 | 
			
		||||
    width: 80px;
 | 
			
		||||
    z-index: 21;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    .logo-img {
 | 
			
		||||
      width: 30px;
 | 
			
		||||
      height: 30px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
.top-menu {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  color: #515a6e;
 | 
			
		||||
  border-bottom: 1px solid #f3f3f3;
 | 
			
		||||
  border-right: 1px solid #f3f3f3;
 | 
			
		||||
}
 | 
			
		||||
.logo {
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  line-height: @header-user-height;
 | 
			
		||||
  padding: 0px 15px 0px 15px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  .logo-img {
 | 
			
		||||
    width: 30px;
 | 
			
		||||
    height: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .title {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    color: v-bind('theme === "light" ? "#001529": "#ffffff"');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 第二列菜单区域
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <a-sub-menu :key="props.menuInfo.menuId.toString()">
 | 
			
		||||
    <template #icon>
 | 
			
		||||
      <component :is="$antIcons[props.menuInfo.icon]" />
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #title>{{ props.menuInfo.menuName }}</template>
 | 
			
		||||
    <template v-for="item in props.menuInfo.children" :key="item.menuId">
 | 
			
		||||
      <template v-if="item.visibleFlag">
 | 
			
		||||
        <template v-if="!item.children">
 | 
			
		||||
          <a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
 | 
			
		||||
            <template #icon>
 | 
			
		||||
              <component :is="$antIcons[item.icon]" />
 | 
			
		||||
            </template>
 | 
			
		||||
            {{ item.menuName }}
 | 
			
		||||
          </a-menu-item>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-else>
 | 
			
		||||
          <SubMenu :menu-info="item" :key="item.menuId" @turnToPage="turnToPage" />
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </template>
 | 
			
		||||
  </a-sub-menu>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup>
 | 
			
		||||
  let props = defineProps({
 | 
			
		||||
    menuInfo: Object,
 | 
			
		||||
  });
 | 
			
		||||
  const emits = defineEmits(['turnToPage']);
 | 
			
		||||
  const turnToPage = (route) => {
 | 
			
		||||
    emits('turnToPage', route);
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
  ::v-deep(.ant-menu-item-selected) {
 | 
			
		||||
    border-right: 3px !important;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 展开菜单 event bus
 | 
			
		||||
 *
 | 
			
		||||
 * @Author:    1024创新实验室-主任:卓大
 | 
			
		||||
 * @Date:      2022-07-12 23:32:48
 | 
			
		||||
 * @Wechat:    zhuda1024
 | 
			
		||||
 * @Email:     lab1024@163.com
 | 
			
		||||
 * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012
 | 
			
		||||
 */
 | 
			
		||||
import mitt from 'mitt';
 | 
			
		||||
export default mitt();
 | 
			
		||||
@@ -0,0 +1,147 @@
 | 
			
		||||
<!--
 | 
			
		||||
  * 第一列菜单
 | 
			
		||||
  *
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大
 | 
			
		||||
  * @Date:      2022-09-06 20:29:12
 | 
			
		||||
  * @Wechat:    zhuda1024
 | 
			
		||||
  * @Email:     lab1024@163.com
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="top-menu-container">
 | 
			
		||||
    <!-- 一级菜单展示 -->
 | 
			
		||||
    <a-menu :selectedKeys="selectedKeys" mode="horizontal" :theme="theme">
 | 
			
		||||
      <template v-for="item in menuTree" :key="item.menuId">
 | 
			
		||||
        <template v-if="item.visibleFlag">
 | 
			
		||||
          <a-menu-item :key="item.menuId.toString()" @click="onSelectMenu(item)">
 | 
			
		||||
            <template #icon>
 | 
			
		||||
              <component :is="$antIcons[item.icon]" />
 | 
			
		||||
            </template>
 | 
			
		||||
            {{ item.menuName }}
 | 
			
		||||
          </a-menu-item>
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </a-menu>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
 | 
			
		||||
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
 | 
			
		||||
import { router } from '/@/router';
 | 
			
		||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
 | 
			
		||||
import menuEmitter from './top-expand-menu-mitt';
 | 
			
		||||
 | 
			
		||||
const websiteName = computed(() => useAppConfigStore().websiteName);
 | 
			
		||||
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
const menuTree = computed(() => useUserStore().getMenuTree || []);
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  collapsed: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
// 展开菜单的顶级目录名字适配,只展示两个字为好
 | 
			
		||||
function menuNameAdapter(name) {
 | 
			
		||||
  return name.substr(0, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选中的顶级菜单
 | 
			
		||||
const selectedKeys = ref([]);
 | 
			
		||||
 | 
			
		||||
// 选中菜单,页面跳转
 | 
			
		||||
function onSelectMenu(menuItem) {
 | 
			
		||||
  selectedKeys.value = [menuItem.menuId.toString()];
 | 
			
		||||
  if (menuItem.menuType === MENU_TYPE_ENUM.MENU.value && (_.isEmpty(menuItem.children) || menuItem.children.every((e) => !e.visibleFlag))) {
 | 
			
		||||
    useUserStore().deleteKeepAliveIncludes(menuItem.menuId.toString());
 | 
			
		||||
    router.push({ name: menuItem.menuId.toString() });
 | 
			
		||||
  }
 | 
			
		||||
  menuEmitter.emit('selectTopMenu', menuItem);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新选中的菜单
 | 
			
		||||
function updateSelectKey(key) {
 | 
			
		||||
  selectedKeys.value = [key];
 | 
			
		||||
  let selectMenu = _.find(menuTree.value, { menuId: Number(key) });
 | 
			
		||||
  if (selectMenu) {
 | 
			
		||||
    menuEmitter.emit('selectTopMenu', selectMenu);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ updateSelectKey });
 | 
			
		||||
 | 
			
		||||
// 动态计算当前导航宽度
 | 
			
		||||
let menuInfo = computed(() => {
 | 
			
		||||
  let width = '100vw';
 | 
			
		||||
  let right = '-100vw';
 | 
			
		||||
  let selectedItem = _.find(menuTree.value, { menuId: Number(selectedKeys.value[0]) });
 | 
			
		||||
  const hasSecoundMenu = selectedItem && !_.isEmpty(selectedItem.children) && selectedItem.children.some((e) => e.visibleFlag);
 | 
			
		||||
  if (hasSecoundMenu) {
 | 
			
		||||
    if (props.collapsed) {
 | 
			
		||||
      width = 'calc(100vw - 80px)';
 | 
			
		||||
      right = 'calc(-100vw + 80px)';
 | 
			
		||||
    } else {
 | 
			
		||||
      width = 'calc(100vw - 180px)';
 | 
			
		||||
      right = 'calc(-100vw + 180px)';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    width,
 | 
			
		||||
    right,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
.top-menu {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  transition: all 0.2s, background 0s;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: v-bind('menuInfo.right');
 | 
			
		||||
  width: v-bind('menuInfo.width');
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
.ant-menu-dark {
 | 
			
		||||
  background: #1677ff;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
.ant-menu-light {
 | 
			
		||||
  background: #1677ff;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
::v-deep(.ant-menu-item-selected) {
 | 
			
		||||
  background: #0958d9 !important;
 | 
			
		||||
  color: #fff !important;
 | 
			
		||||
}
 | 
			
		||||
.top-menu-container {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
.logo {
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  line-height: @header-user-height;
 | 
			
		||||
  padding: 0px 15px 0px 15px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  .logo-img {
 | 
			
		||||
    width: 30px;
 | 
			
		||||
    height: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .title {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    color: v-bind('theme === "light" ? "#001529": "#ffffff"');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -14,6 +14,8 @@
 | 
			
		||||
  <SideExpandLayout v-if="layout === LAYOUT_ENUM.SIDE_EXPAND.value" />
 | 
			
		||||
  <!--顶部菜单 模式-->
 | 
			
		||||
  <TopLayout v-if="layout === LAYOUT_ENUM.TOP.value" />
 | 
			
		||||
    <!--顶部展开 模式-->
 | 
			
		||||
  <TopExpandLayout v-if="layout === LAYOUT_ENUM.TOP_EXPAND.value" />
 | 
			
		||||
  <!--定期修改密码-->
 | 
			
		||||
  <RegularChangePasswordModal />
 | 
			
		||||
</template>
 | 
			
		||||
@@ -21,6 +23,7 @@
 | 
			
		||||
  import { computed } from 'vue';
 | 
			
		||||
  import { LAYOUT_ENUM } from '/@/constants/layout-const';
 | 
			
		||||
  import SideExpandLayout from './side-expand-layout.vue';
 | 
			
		||||
  import TopExpandLayout from './top-expand-layout.vue';
 | 
			
		||||
  import SideLayout from './side-layout.vue';
 | 
			
		||||
  import TopLayout from './top-layout.vue';
 | 
			
		||||
  import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										279
									
								
								smart-admin-web-typescript/src/layout/top-expand-layout.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								smart-admin-web-typescript/src/layout/top-expand-layout.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
<!--
 | 
			
		||||
  *  展开菜单模式
 | 
			
		||||
  * 
 | 
			
		||||
  * @Author:    1024创新实验室-主任:卓大 
 | 
			
		||||
  * @Date:      2022-09-06 20:40:16 
 | 
			
		||||
  * @Wechat:    zhuda1024 
 | 
			
		||||
  * @Email:     lab1024@163.com 
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012 
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <a-layout class="admin-layout" style="min-height: 100%">
 | 
			
		||||
    <!-- 侧边菜单 side-menu -->
 | 
			
		||||
    <a-layout-sider :theme="theme" class="side-menu" :collapsed="collapsed" :trigger="null">
 | 
			
		||||
      <!-- 左侧菜单 -->
 | 
			
		||||
      <TopExpandMenu :collapsed="collapsed" />
 | 
			
		||||
    </a-layout-sider>
 | 
			
		||||
 | 
			
		||||
    <!--中间内容,一共三部分:1、顶部;2、中间内容区域;3、底部(一般是公司版权信息);-->
 | 
			
		||||
    <a-layout class="admin-layout-main" :style="`height: ${windowHeight}px`" id="smartAdminMain">
 | 
			
		||||
      <!-- 顶部头部信息 -->
 | 
			
		||||
      <a-layout-header class="smart-layout-header">
 | 
			
		||||
        <a-row justify="space-between" class="smart-layout-header-user">
 | 
			
		||||
          <a-col class="smart-layout-header-left">
 | 
			
		||||
            <span class="collapsed-button">
 | 
			
		||||
              <menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
 | 
			
		||||
              <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
 | 
			
		||||
            </span>
 | 
			
		||||
            <a-tooltip placement="bottom">
 | 
			
		||||
              <template #title>首页</template>
 | 
			
		||||
              <span class="home-button" @click="goHome">
 | 
			
		||||
                <home-outlined class="trigger" />
 | 
			
		||||
              </span>
 | 
			
		||||
            </a-tooltip>
 | 
			
		||||
            <span class="location-breadcrumb">
 | 
			
		||||
              <MenuLocationBreadcrumb />
 | 
			
		||||
            </span>
 | 
			
		||||
          </a-col>
 | 
			
		||||
          <!---用戶操作区域-->
 | 
			
		||||
          <a-col class="smart-layout-header-right">
 | 
			
		||||
            <HeaderUserSpace />
 | 
			
		||||
          </a-col>
 | 
			
		||||
        </a-row>
 | 
			
		||||
        <PageTag />
 | 
			
		||||
      </a-layout-header>
 | 
			
		||||
 | 
			
		||||
      <!--中间内容-->
 | 
			
		||||
      <a-layout-content class="admin-layout-content" id="smartAdminLayoutContent">
 | 
			
		||||
        <!--不keepAlive的iframe使用单个iframe组件-->
 | 
			
		||||
        <IframeIndex v-show="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
 | 
			
		||||
        <!--keepAlive的iframe 每个页面一个iframe组件-->
 | 
			
		||||
        <IframeIndex
 | 
			
		||||
          v-for="item in keepAliveIframePages"
 | 
			
		||||
          v-show="route.name == item.name"
 | 
			
		||||
          :key="item.name"
 | 
			
		||||
          :name="item.name"
 | 
			
		||||
          :url="item.meta.frameUrl"
 | 
			
		||||
        />
 | 
			
		||||
        <!--非iframe使用router-view-->
 | 
			
		||||
        <div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)">
 | 
			
		||||
          <router-view v-slot="{ Component }">
 | 
			
		||||
            <keep-alive :include="keepAliveIncludes">
 | 
			
		||||
              <component :is="Component" :key="route.name" />
 | 
			
		||||
            </keep-alive>
 | 
			
		||||
          </router-view>
 | 
			
		||||
        </div>
 | 
			
		||||
      </a-layout-content>
 | 
			
		||||
      <!-- footer 版权公司信息 -->
 | 
			
		||||
      <a-layout-footer class="smart-layout-footer" v-show="footerFlag"> <SmartFooter /></a-layout-footer>
 | 
			
		||||
      <!---- 回到顶部 --->
 | 
			
		||||
      <a-back-top :target="backTopTarget" :visibilityHeight="80" />
 | 
			
		||||
    </a-layout>
 | 
			
		||||
    <!-- 右侧帮助文档 help-doc -->
 | 
			
		||||
    <a-layout-sider v-show="helpDocFlag" theme="light" :width="180" class="help-doc-sider" :trigger="null" style="min-height: 100%">
 | 
			
		||||
      <SideHelpDoc />
 | 
			
		||||
    </a-layout-sider>
 | 
			
		||||
  </a-layout>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, onMounted, ref, watch } from 'vue';
 | 
			
		||||
import HeaderUserSpace from './components/header-user-space/index.vue';
 | 
			
		||||
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
 | 
			
		||||
import PageTag from './components/page-tag/index.vue';
 | 
			
		||||
import TopExpandMenu from './components/top-expand-menu/index.vue';
 | 
			
		||||
import SmartFooter from './components/smart-footer/index.vue';
 | 
			
		||||
import { smartKeepAlive } from './components/smart-keep-alive';
 | 
			
		||||
import IframeIndex from '/@/components/framework/iframe/iframe-index.vue';
 | 
			
		||||
import watermark from '../lib/smart-watermark';
 | 
			
		||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
 | 
			
		||||
import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
import SideHelpDoc from './components/side-help-doc/index.vue';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
 | 
			
		||||
 | 
			
		||||
const windowHeight = ref(window.innerHeight);
 | 
			
		||||
 | 
			
		||||
//主题颜色
 | 
			
		||||
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
//是否显示标签页
 | 
			
		||||
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
 | 
			
		||||
// 是否显示帮助文档
 | 
			
		||||
const helpDocFlag = computed(() => useAppConfigStore().$state.helpDocExpandFlag);
 | 
			
		||||
// 是否显示页脚
 | 
			
		||||
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
 | 
			
		||||
// 是否显示水印
 | 
			
		||||
const watermarkFlag = computed(() => useAppConfigStore().$state.watermarkFlag);
 | 
			
		||||
// 多余高度
 | 
			
		||||
const dueHeight = computed(() => {
 | 
			
		||||
  let due = 40;
 | 
			
		||||
  if (useAppConfigStore().$state.pageTagFlag) {
 | 
			
		||||
    due = due + 40;
 | 
			
		||||
  }
 | 
			
		||||
  if (useAppConfigStore().$state.footerFlag) {
 | 
			
		||||
    due = due + 40;
 | 
			
		||||
  }
 | 
			
		||||
  return due;
 | 
			
		||||
});
 | 
			
		||||
//是否隐藏菜单
 | 
			
		||||
const collapsed = ref(false);
 | 
			
		||||
 | 
			
		||||
//页面初始化的时候加载水印
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (watermarkFlag.value) {
 | 
			
		||||
    watermark.set('smartAdminLayoutContent', useUserStore().actualName);
 | 
			
		||||
  } else {
 | 
			
		||||
    watermark.clear();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => watermarkFlag.value,
 | 
			
		||||
  (newValue) => {
 | 
			
		||||
    if (newValue) {
 | 
			
		||||
      watermark.set('smartAdminLayoutContent', useUserStore().actualName);
 | 
			
		||||
    } else {
 | 
			
		||||
      watermark.clear();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
window.addEventListener('resize', function () {
 | 
			
		||||
  windowHeight.value = window.innerHeight;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//回到顶部
 | 
			
		||||
const backTopTarget = () => {
 | 
			
		||||
  return document.getElementById('smartAdminMain');
 | 
			
		||||
};
 | 
			
		||||
// ----------------------- keep-alive相关 -----------------------
 | 
			
		||||
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
function goHome() {
 | 
			
		||||
  router.push({ name: HOME_PAGE_NAME });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
:deep(.ant-layout-header) {
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
:deep(.layout-header) {
 | 
			
		||||
  height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header {
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  z-index: 21;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header-user {
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
  border-bottom: 1px solid #f6f6f6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header-left {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
 | 
			
		||||
  .collapsed-button {
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-button {
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 0 5px;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-button:hover {
 | 
			
		||||
    background-color: #efefef;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .location-breadcrumb {
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    line-height: @header-user-height;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-header-right {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: @header-user-height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.admin-layout {
 | 
			
		||||
  .side-menu {
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    flex: 0 !important;
 | 
			
		||||
    min-width: inherit !important;
 | 
			
		||||
    max-width: none !important;
 | 
			
		||||
    // width: auto !important;
 | 
			
		||||
    &.fixed-side {
 | 
			
		||||
      position: fixed;
 | 
			
		||||
      height: 100vh;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      top: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .side-menu::-webkit-scrollbar {
 | 
			
		||||
    width: 4px;
 | 
			
		||||
  }
 | 
			
		||||
  .side-menu::-webkit-scrollbar-thumb {
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.2);
 | 
			
		||||
  }
 | 
			
		||||
  .side-menu::-webkit-scrollbar-track {
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .help-doc-sider {
 | 
			
		||||
    flex: 0 !important;
 | 
			
		||||
    min-width: 100px;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
    width: auto !important;
 | 
			
		||||
    &.fixed-side {
 | 
			
		||||
      position: fixed;
 | 
			
		||||
      height: 100vh;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      top: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .virtual-side {
 | 
			
		||||
    transition: all 0.2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .virtual-header {
 | 
			
		||||
    transition: all 0.2s;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
 | 
			
		||||
    &.fixed-tabs.multi-page:not(.fixed-header) {
 | 
			
		||||
      height: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .admin-layout-main {
 | 
			
		||||
    padding-top: 46px;
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .admin-layout-content {
 | 
			
		||||
    background-color: inherit;
 | 
			
		||||
    min-height: auto;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    padding: 10px 10px 0px 10px;
 | 
			
		||||
    height: calc(100% - v-bind(dueHeight) px);
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smart-layout-footer {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Reference in New Issue
	
	Block a user