first commit

This commit is contained in:
maxf 2025-07-31 13:26:17 +08:00
commit fa9e69baa5
89 changed files with 62699 additions and 0 deletions

8
README.md Normal file
View File

@ -0,0 +1,8 @@
权限角色管理系统
---
#### 依赖框架
*[x] springboot 3.5.4
*[x] thymeleaf
*[x] jpa
*[x] mysql

259
mvnw vendored Normal file
View File

@ -0,0 +1,259 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

149
mvnw.cmd vendored Normal file
View File

@ -0,0 +1,149 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

101
pom.xml Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.yexuejc</groupId>
<artifactId>permission-role-management-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>permission-role-management-system</name>
<description>permission-role-management-system</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>top.yexuejc</groupId>
<artifactId>yexuejc-base</artifactId>
<version>1.5.3-jre11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package top.yexuejc.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PermissionRoleManagementSystemApplication {
public static void main(String[] args) {
SpringApplication.run(PermissionRoleManagementSystemApplication.class, args);
}
}

View File

@ -0,0 +1,48 @@
package top.yexuejc.admin.config;
import java.util.Locale;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import top.yexuejc.admin.constant.SymbolsConstant;
/**
*
* @author maxiaofeng
* @date 2025/7/29 18:33
*/
@Configuration
public class I18nConfig {
private static final Logger log = LoggerFactory.getLogger(I18nConfig.class);
@Value("${default.locale:zh-CN}")
private String defaultLocale;
@Value("${spring.messages.basename:messages}")
private String basename;
@Value("${spring.messages.encoding:UTF-8}")
private String encoding;
@Value("${spring.messages.cache.seconds:3600}")
private int cacheSeconds;
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(basename.split(SymbolsConstant.COMMA));
messageSource.setDefaultEncoding(encoding);
messageSource.setCacheSeconds(cacheSeconds);
return messageSource;
}
@PostConstruct
public void setDefaultLocale() {
Locale locale = Locale.forLanguageTag(defaultLocale);
log.debug("Local default language:{}", locale);
Locale.setDefault(locale);
}
}

View File

@ -0,0 +1,17 @@
package top.yexuejc.admin.constant;
/**
* @author maxiaofeng
* @date 2025/7/29 20:19
*/
public enum MenuTypeEnum {
MENU("1","菜单目录"),
PATH("2","菜单"),
BUTTON("3","按钮");
public String code;
public String desc;
MenuTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}

View File

@ -0,0 +1,21 @@
package top.yexuejc.admin.constant;
/**
* @author maxiaofeng
* @date 2025/7/29 19:05
*/
public enum MessageCodes {
SUCCESS("SUCCESS","OK"),
FAIL("FAIL","ERROR"),
USER_NOT_EXIST("USER_NOT_EXIST","用户不存在"),
USER_PASSWORD_ERROR("USER_PASSWORD_ERROR","用户密码错误"),
USER_NOT_LOGIN("USER_NOT_LOGIN","用户未登录"),
USER_NOT_PERMISSION("USER_NOT_PERMISSION","用户无权限"),
;
public String code;
public String message;
MessageCodes(String code, String message) {
this.code = code;
this.message = message;
}
}

View File

@ -0,0 +1,13 @@
package top.yexuejc.admin.constant;
/**
* 符号常量
*
* @author maxiaofeng
* @date 2025/7/29 18:32
*/
public class SymbolsConstant {
/** 半角逗号 */
public static final String COMMA = ",";
public static final String EMPTY = "";
}

View File

@ -0,0 +1,40 @@
package top.yexuejc.admin.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
/**
* 菜单
*
* @author maxiaofeng
* @date 2025/7/29 18:45
*/
@Entity
@Table(name = "sys_menu")
@Data
public class Menu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 菜单名称 */
private String name;
/**
* 菜单类型
* 1MENU
* 2PATH
* 3按钮
*/
private String type;
/**
* 菜单类型为2时为path
* 菜单类型为3时为按钮标识
*/
private String tag;
/** 父级菜单ID */
private Long parentId;
private Integer orderNum;
}

View File

@ -0,0 +1,40 @@
package top.yexuejc.admin.entity;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
/**
* 角色
*
* @author maxiaofeng
* @date 2025/7/29 18:48
*/
@Entity
@Table(name = "sys_role")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 角色名称 */
@Column(unique = true, nullable = false)
private String name;
/**
* 角色级别
* 值越小级别越高
* 默认为1
* 值大的看不见值小的
* */
@Column(unique = true, nullable = false)
private Integer level;
private String description;
private Date createTime;
}

View File

@ -0,0 +1,22 @@
package top.yexuejc.admin.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
/**
* 角色菜单关联
* @author maxiaofeng
* @date 2025/7/29 18:55
*/
@Entity
@Table(name = "sys_role_menu")
@Data
public class RoleMenu {
@Id
private Long roleId;
@Id
private Long menuId;
}

View File

@ -0,0 +1,34 @@
package top.yexuejc.admin.entity;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
/**
* 用户
* @author maxiaofeng
* @date 2025/7/29 18:44
*/
@Entity
@Table(name = "sys_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String name;
/**状态0禁用1启用*/
private Integer status;
private Date createTime;
}

View File

@ -0,0 +1,22 @@
package top.yexuejc.admin.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
/**
* 用户角色关联表
* @author maxiaofeng
* @date 2025/7/29 18:54
*/
@Entity
@Table(name = "sys_user_role")
@Data
public class UserRole {
@Id
private Long userId;
@Id
private Long roleId;
}

View File

@ -0,0 +1,12 @@
package top.yexuejc.admin.exception;
/**
* @author maxiaofeng
* @date 2025/7/30 13:34
*/
public class AppException extends Exception{
public AppException(String message) {
super(message);
}
}

View File

@ -0,0 +1,55 @@
package top.yexuejc.admin.model;
import java.io.Serializable;
/**
* 外部APIレスポンスmodel
*
* @author ISC馬
* @date 2023/08/28
*/
public class HttpRespBO implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String body;
private String cookie;
public HttpRespBO() {
}
public HttpRespBO(Integer code, String body) {
super();
this.code = code;
this.body = body;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getCookie() {
return cookie;
}
public HttpRespBO setCookie(String cookie) {
this.cookie = cookie;
return this;
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,88 @@
package top.yexuejc.admin.model;
import java.util.List;
import top.yexuejc.admin.constant.MessageCodes;
/**
* API結果が戻りする集合型
*
* @author ISC
* @date 2023/08/18
*/
public class ListResponseVO<T> extends ResponseVO {
@SuppressWarnings("java:S1948")// オブジェクトタイプは直列化する必要があります
private List<T> data;
public ListResponseVO() {
super();
}
public ListResponseVO(List<T> data) {
this.data = data;
setCode(MessageCodes.SUCCESS.code);
setMessage(MessageCodes.SUCCESS.message);
}
public ListResponseVO(List<T> data, String code, String msg) {
super(code, msg);
this.data = data;
}
public List<T> getData() {
return data;
}
public ListResponseVO<T> setData(List<T> data) {
this.data = data;
return this;
}
public static <T> ListResponseVO<T> success(List<T> data) {
return new ListResponseVO<>(data);
}
public static ListResponseVO<String> fail(String message) {
ListResponseVO<String> vo = new ListResponseVO<>();
vo.setCode(MessageCodes.FAIL.code).setMessage(message);
return vo;
}
public static ListResponseVO<String> success(String errorCode, String message) {
ListResponseVO<String> vo = new ListResponseVO<>();
vo.setCode(errorCode).setMessage(message);
return vo;
}
@Override
public ListResponseVO<T> setCode(String code) {
super.setCode(code);
return this;
}
@Override
public ListResponseVO<T> setMessage(String message) {
super.setMessage(message);
return this;
}
@Override
public ListResponseVO<T> setErrCode(String errCode) {
super.setErrCode(errCode);
return this;
}
@Override
public ListResponseVO<T> setErrCodeDes(String errCodeDes) {
super.setErrCodeDes(errCodeDes);
return this;
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,84 @@
package top.yexuejc.admin.model;
import top.yexuejc.admin.constant.MessageCodes;
/**
* API結果が戻りする対象型
*
* @author ISC
* @date 2023/08/18
*/
public class ObjectResponseVO<T> extends ResponseVO {
@SuppressWarnings("java:S1948") // オブジェクトタイプは直列化する必要があります
private T data;
public ObjectResponseVO() {
}
public ObjectResponseVO(T data) {
this.data = data;
setCode(MessageCodes.SUCCESS.code);
setMessage(MessageCodes.SUCCESS.message);
}
public ObjectResponseVO(T data, String code, String msg) {
super(code, msg);
this.data = data;
}
public T getData() {
return data;
}
public ObjectResponseVO<T> setData(T data) {
this.data = data;
return this;
}
public static <T> ObjectResponseVO<T> success(T data) {
return new ObjectResponseVO<>(data);
}
public static ObjectResponseVO<String> fail(String message) {
ObjectResponseVO<String> vo = new ObjectResponseVO<>();
vo.setCode(MessageCodes.FAIL.code).setMessage(message);
return vo;
}
public static ObjectResponseVO<String> fail(String errorCode, String message) {
ObjectResponseVO<String> vo = new ObjectResponseVO<>();
vo.setCode(errorCode).setMessage(message);
return vo;
}
@Override
public ObjectResponseVO<T> setCode(String code) {
super.setCode(code);
return this;
}
@Override
public ObjectResponseVO<T> setMessage(String message) {
super.setMessage(message);
return this;
}
@Override
public ObjectResponseVO<T> setErrCode(String errCode) {
super.setErrCode(errCode);
return this;
}
@Override
public ObjectResponseVO<T> setErrCodeDes(String errCodeDes) {
super.setErrCodeDes(errCodeDes);
return this;
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,113 @@
package top.yexuejc.admin.model;
import java.util.List;
/**
* API結果が戻りする集合型
*
* @author maxiaofeng
* @date 2025/7/9 15:23
*/
public class PageResponseVO<T> extends ListResponseVO<T> {
/**
* 総件数
*/
private Long totalCount;
/**
* ページサイズ
*/
private Long pageSize;
/**
* 現在のページ
*/
private Long pageNum;
public PageResponseVO() {
super();
}
public PageResponseVO(List<T> data, long count) {
super(data);
this.totalCount = count;
}
public static <T> PageResponseVO<T> of(List<T> data, long count) {
return new PageResponseVO<>(data, count);
}
public PageResponseVO<T> withOffset(long offset, long limit) {
this.pageNum = offset / limit + 1;
this.pageSize = limit;
return this;
}
public PageResponseVO<T> withPageNum(long pageNum, long pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
return this;
}
public Long getTotalCount() {
return totalCount;
}
public PageResponseVO<T> setTotalCount(Long totalCount) {
this.totalCount = totalCount;
return this;
}
public Long getPageSize() {
return pageSize;
}
public PageResponseVO<T> setPageSize(Long pageSize) {
this.pageSize = pageSize;
return this;
}
public Long getPageNum() {
return pageNum;
}
public PageResponseVO<T> setPageNum(Long pageNum) {
this.pageNum = pageNum;
return this;
}
@Override
public PageResponseVO<T> setCode(String code) {
super.setCode(code);
return this;
}
@Override
public PageResponseVO<T> setMessage(String message) {
super.setMessage(message);
return this;
}
@Override
public PageResponseVO<T> setErrCode(String errCode) {
super.setErrCode(errCode);
return this;
}
@Override
public PageResponseVO<T> setErrCodeDes(String errCodeDes) {
super.setErrCodeDes(errCodeDes);
return this;
}
@Override
public PageResponseVO<T> setData(List<T> data) {
super.setData(data);
return this;
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,102 @@
package top.yexuejc.admin.model;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
import top.yexuejc.admin.constant.MessageCodes;
import top.yexuejc.admin.constant.SymbolsConstant;
/**
* API結果が戻りする
*
* @author ISC
* @date 2023/08/18
*/
public class ResponseVO implements Serializable {
@JsonProperty("return_code")
private String code;
@JsonProperty("return_msg")
private String message;
/** APP用 */
@JsonProperty("err_code")
private String errCode;
/** APP用 */
@JsonProperty("err_code_des")
private String errCodeDes;
public ResponseVO() {
}
public ResponseVO(String code, String msg) {
this.code = code;
this.message = msg;
}
public String getCode() {
return code;
}
public ResponseVO setCode(String code) {
this.code = code;
return this;
}
public String getMessage() {
return message;
}
public ResponseVO setMessage(String message) {
this.message = message;
return this;
}
public String getErrCode() {
return errCode;
}
public ResponseVO setErrCode(String errCode) {
this.errCode = errCode;
return this;
}
public String getErrCodeDes() {
return errCodeDes;
}
public ResponseVO setErrCodeDes(String errCodeDes) {
this.errCodeDes = errCodeDes;
return this;
}
public static ResponseVO success() {
return new ResponseVO(MessageCodes.SUCCESS.code, SymbolsConstant.EMPTY);
}
public static ResponseVO success(String code, String message) {
return new ResponseVO(code, message);
}
public static ResponseVO fail(String message) {
return new ResponseVO(MessageCodes.FAIL.code, message);
}
public static ResponseVO fail4Code(String errorCode) {
return new ResponseVO(errorCode, SymbolsConstant.EMPTY);
}
public static ResponseVO fail(String errorCode, String message) {
return new ResponseVO(errorCode, message);
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,20 @@
package top.yexuejc.admin.model.input;
import java.io.Serializable;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author maxiaofeng
* @date 2025/7/29 19:50
*/
@Data
@Accessors(chain = true)
public class LoginInput implements Serializable {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@ -0,0 +1,32 @@
package top.yexuejc.admin.model.input;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author maxiaofeng
* @date 2025/7/29 21:49
*/
@Data
@Accessors(chain = true)
public class PageInput implements Serializable {
private int pageIndex;
private int pageSize;
public int getPageIndex() {
if (pageIndex <= 0) {
pageIndex = 1;
}
return pageIndex;
}
public int getPageSize() {
if (pageSize <= 0) {
pageSize = 10;
}
return pageSize;
}
}

View File

@ -0,0 +1,16 @@
package top.yexuejc.admin.model.input;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @author maxiaofeng
* @date 2025/7/29 21:50
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class UserSearch extends PageInput {
private String username;
}

View File

@ -0,0 +1,91 @@
package top.yexuejc.admin.model.output;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.EqualsAndHashCode;
import top.yexuejc.admin.entity.Menu;
/**
* @author maxiaofeng
* @date 2025/7/29 20:21
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class MenuTree extends Menu implements Serializable {
private List<MenuTree> children = new ArrayList<>();
public static MenuTree build(Menu menu) {
MenuTree menuTree = new MenuTree();
menuTree.setId(menu.getId());
menuTree.setName(menu.getName());
menuTree.setType(menu.getType());
menuTree.setTag(menu.getTag());
menuTree.setParentId(menu.getParentId());
menuTree.setOrderNum(menu.getOrderNum());
return menuTree;
}
/**
* 将菜单列表构建成树状结构
*
* @param menuList 菜单列表
* @return 树状菜单结构
*/
public static List<MenuTree> buildMenuTree(List<Menu> menuList) {
// 使用Map存储所有菜单项便于快速查找
Map<Long, MenuTree> menuMap = new HashMap<>();
// 第一次遍历创建所有菜单节点
for (Menu menu : menuList) {
menuMap.put(menu.getId(), MenuTree.build(menu));
}
// 根节点列表
List<MenuTree> rootMenus = new ArrayList<>();
// 第二次遍历建立父子关系
for (Menu menu : menuList) {
MenuTree menuTree = menuMap.get(menu.getId());
Long parentId = menu.getParentId();
if (parentId == null || parentId == 0) {
// 没有父节点的为根节点
rootMenus.add(menuTree);
} else {
// 有父节点的建立父子关系
MenuTree parentMenuTree = menuMap.get(parentId);
if (parentMenuTree != null) {
parentMenuTree.getChildren().add(menuTree);
}
}
}
// 对菜单进行排序
rootMenus.sort(Comparator.comparing(MenuTree::getOrderNum));
sortChildren(rootMenus);
return rootMenus;
}
/**
* 递归对子菜单进行排序
*
* @param menus 菜单列表
*/
private static void sortChildren(List<MenuTree> menus) {
for (MenuTree menu : menus) {
List<MenuTree> children = menu.getChildren();
if (children != null && !children.isEmpty()) {
children.sort(Comparator.comparing(MenuTree::getOrderNum));
sortChildren(children);
}
}
}
}

View File

@ -0,0 +1,20 @@
package top.yexuejc.admin.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import top.yexuejc.admin.entity.Menu;
import top.yexuejc.admin.entity.Role;
/**
* @author maxiaofeng
* @date 2025/7/29 20:01
*/
public interface MenuRepository extends JpaRepository<Menu, Long> {
@Query("select m from Menu m, RoleMenu rm, UserRole ur where m.id = rm.menuId and rm.roleId = ur.roleId and ur.userId = ?1")
List<Menu> findAllByUserId(Long userId);
@Query("select m from Menu m, RoleMenu rm where m.id = rm.menuId and rm.roleId = ?1")
List<Menu> findAllByRoleId(Long roleId);
}

View File

@ -0,0 +1,20 @@
package top.yexuejc.admin.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import top.yexuejc.admin.entity.Role;
import top.yexuejc.admin.entity.UserRole;
/**
* @author maxiaofeng
* @date 2025/7/29 20:02
*/
public interface RoleRepository extends JpaRepository<Role, Long> {
@Query("select r from Role r, UserRole ur where r.id = ur.roleId and ur.userId = ?1")
List<Role> findAllByUserId(Long userId);
List<Role> findByLevelGreaterThanEqual(int level);
}

View File

@ -0,0 +1,16 @@
package top.yexuejc.admin.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import top.yexuejc.admin.entity.User;
/**
* @author maxiaofeng
* @date 2025/7/29 20:00
*/
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
Page<User> findByUsernameContaining(String username, Pageable pageable);
}

View File

@ -0,0 +1,27 @@
package top.yexuejc.admin.util;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.yexuejc.admin.entity.User;
/**
* @author maxiaofeng
* @date 2025/7/30 13:30
*/
public class HttpUtil {
public static User getLoginUser() {
User user = null;
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (null != attributes) {
HttpServletRequest request = attributes.getRequest();
user = (User) request.getSession().getAttribute("user");
}
} catch (Exception e) {
// ignored Exception
}
return user;
}
}

View File

@ -0,0 +1,106 @@
package top.yexuejc.admin.web;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.yexuejc.base.util.JsonUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.yexuejc.admin.entity.Menu;
import top.yexuejc.admin.entity.User;
import top.yexuejc.admin.model.ResponseVO;
import top.yexuejc.admin.model.input.LoginInput;
import top.yexuejc.admin.model.output.MenuTree;
import top.yexuejc.admin.repository.MenuRepository;
import top.yexuejc.admin.repository.UserRepository;
/**
* @author maxiaofeng
* @date 2025/7/29 18:56
*/
@RestController
public class LoginCtrl {
private static final Logger log = LoggerFactory.getLogger(LoginCtrl.class);
@Resource
private UserRepository userRepository;
@Resource
private MenuRepository menuRepository;
/**
* 登录处理
*
* @return
*/
@PostMapping(value = "/login", consumes = "application/x-www-form-urlencoded")
public ResponseVO login(@RequestParam String username, @RequestParam String password, HttpServletRequest request, HttpServletResponse response) {
return login(new LoginInput().setUsername(username).setPassword(password), request, response);
}
@PostMapping(value = "/login", consumes = "application/json")
public ResponseVO login(@RequestBody @Validated LoginInput loginInput, HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
// 用户存在
User user = userRepository.findByUsername(loginInput.getUsername());
if (user == null) {
return ResponseVO.fail("用户不存在");
}
// 密码正确
if (!user.getPassword().equals(loginInput.getPassword())) {
return ResponseVO.fail("密码错误");
}
// 用户状态正常
if (user.getStatus() != 1) {
return ResponseVO.fail("用户状态异常");
}
// 登录成功
session.setAttribute("user", user);
setCookie("user", JsonUtil.obj2Json(user), response);
// 获取用户角色=>菜单
List<Menu> menuList = menuRepository.findAllByUserId(user.getId());
if (menuList == null || menuList.isEmpty()) {
return ResponseVO.success();
}
// 通过Menu.parentId来整理菜单为树状结构
List<MenuTree> menuTree = MenuTree.buildMenuTree(menuList);
session.setAttribute("menuTree", menuTree);
// 把menuTree设置到cookie
setCookie("menuTree", JsonUtil.obj2Json(menuTree), response);
System.out.println(JsonUtil.obj2Json(user));
System.out.println(JsonUtil.obj2Json(menuTree));
return ResponseVO.success();
}
private static void setCookie(String name, String value, HttpServletResponse response) {
log.info("setCookie name:{},value:{}", name, value);
Cookie cookie = new Cookie(name, URLEncoder.encode(value, StandardCharsets.UTF_8));
cookie.setPath("/");
cookie.setMaxAge(30 * 60); // 设置cookie有效期为30分钟
response.addCookie(cookie);
}
@RequestMapping("/logout")
public ResponseVO lougout(HttpServletRequest request) {
HttpSession session = request.getSession();
session.invalidate();
return ResponseVO.success();
}
}

View File

@ -0,0 +1,53 @@
package top.yexuejc.admin.web;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.yexuejc.base.util.JsonUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.yexuejc.admin.model.ListResponseVO;
import top.yexuejc.admin.model.output.MenuTree;
/**
* @author maxiaofeng
* @date 2025/7/29 21:15
*/
@RestController
@RequestMapping("/menu")
public class MenuCtrl {
@RequestMapping("/list")
public ListResponseVO<MenuTree> getMenuTree(HttpServletRequest request) {
HttpSession session = request.getSession();
List<MenuTree> menuTree = (List<MenuTree>) session.getAttribute("menuTree");
if (menuTree == null) {
//从cookie中获取
menuTree = getByCookie(request);
}
return ListResponseVO.success(menuTree);
}
private List<MenuTree> getByCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("menuTree".equals(cookie.getName())) {
try {
String menuTreeJson = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
return JsonUtil.json2Obj(menuTreeJson, new TypeReference<List<MenuTree>>() {
});
} catch (Exception e) {
break;
}
}
}
}
return null;
}
}

View File

@ -0,0 +1,33 @@
package top.yexuejc.admin.web;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
/**
* 画面跳转
*
* @author maxiaofeng
* @date 2025/7/29 18:58
*/
@Controller
public class PageCtrl {
@RequestMapping("/view/{uri}")
public ModelAndView index(@PathVariable String uri, @RequestParam Map<String, String> params) {
ModelAndView mv = new ModelAndView("view/" + uri);
mv.addObject("params", params);
return mv;
}
@RequestMapping("/v/{*uri}")
public ModelAndView bizPage(@PathVariable String uri, @RequestParam Map<String, String> params) {
ModelAndView mv = new ModelAndView(uri);
mv.addObject("params", params);
return mv;
}
}

View File

@ -0,0 +1,56 @@
package top.yexuejc.admin.web;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.yexuejc.admin.entity.Menu;
import top.yexuejc.admin.entity.Role;
import top.yexuejc.admin.entity.User;
import top.yexuejc.admin.exception.AppException;
import top.yexuejc.admin.model.ListResponseVO;
import top.yexuejc.admin.model.output.MenuTree;
import top.yexuejc.admin.repository.MenuRepository;
import top.yexuejc.admin.repository.RoleRepository;
import top.yexuejc.admin.util.HttpUtil;
/**
* @author maxiaofeng
* @date 2025/7/30 10:38
*/
@RestController
@RequestMapping("/role")
public class RoleCtrl {
@Resource
private RoleRepository roleRepository;
@Resource
private MenuRepository menuRepository;
@RequestMapping("/list")
public ListResponseVO<Role> getRoleList() throws AppException {
User loginUser = HttpUtil.getLoginUser();
if (loginUser == null) {
throw new AppException("请先登录");
}
//获取用户角色中等级最高的角色
List<Role> roleList = roleRepository.findAllByUserId(loginUser.getId());
int minLevel =
Optional.ofNullable(roleList).orElse(Collections.emptyList()).stream().min(Comparator.comparingInt(Role::getLevel)).map(Role::getLevel).orElse(99);
// 获取level大于等于minLevel的所有角色
List<Role> all = roleRepository.findByLevelGreaterThanEqual(minLevel);
return ListResponseVO.success(all);
}
@RequestMapping("/{id}/menus")
public ListResponseVO<MenuTree> getMenusByRoleId(@PathVariable Long id) {
List<Menu> menuList = menuRepository.findAllByRoleId(id);
//根据菜单的parentId来整理菜单为树状结构
List<MenuTree> menuTree = MenuTree.buildMenuTree(menuList);
return ListResponseVO.success(menuTree);
}
}

View File

@ -0,0 +1,40 @@
package top.yexuejc.admin.web;
import com.yexuejc.base.util.JsonUtil;
import jakarta.annotation.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.yexuejc.admin.entity.User;
import top.yexuejc.admin.model.PageResponseVO;
import top.yexuejc.admin.model.input.UserSearch;
import top.yexuejc.admin.repository.UserRepository;
/**
* @author maxiaofeng
* @date 2025/7/29 21:48
*/
@RestController
@RequestMapping("/user")
public class UserCtrl {
@Resource
private UserRepository userRepository;
@RequestMapping("/list")
public PageResponseVO<User> getUserList(UserSearch userSearch) {
Sort sortComment = Sort.by(Sort.Order.asc("id"));
Pageable pageableComment = PageRequest.of(userSearch.getPageIndex() - 1, userSearch.getPageSize(), sortComment);
Page<User> all;
if (userSearch.getUsername() != null && !userSearch.getUsername().isEmpty()) {
all = userRepository.findByUsernameContaining(userSearch.getUsername(), pageableComment);
} else {
all = userRepository.findAll(pageableComment);
}
System.out.println(JsonUtil.obj2Json(all));
return PageResponseVO.of(all.getContent(), all.getTotalElements());
}
}

View File

@ -0,0 +1,50 @@
spring.application.name=permission-role-management-system
# Thymeleaf \u914D\u7F6E
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
# \u8BBE\u5B9A\u9759\u6001\u8D44\u6E90\u7F13\u5B58(\u9700\u8981\u7F13\u5B58\u4F7F\u7528false)
spring.web.resources.chain.strategy.content.enabled=false
spring.web.resources.chain.strategy.content.paths=/**
# \u5173\u95ED\u4E25\u683C\u8D44\u6E90\u68C0\u67E5
spring.mvc.static-path-pattern=/**
# \u591A\u8BED\u8A00
spring.messages.basename=classpath:i18n/messages,classpath:i18n/layout
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=true
default.locale=ja-JP
# DB
spring.datasource.url=jdbc:mysql://192.168.5.125:3308/test_role?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=intasect
spring.datasource.password=2$hGv1Dz[3)J7]BS
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=300
spring.datasource.hikari.minimum-idle=20
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.idle-timeout=10000
# log
#\u914D\u7F6E\u5168\u5C40\u8F93\u51FA\u7EA7\u522B off<trace<debug<info<warn<error<fatal
logging.level.root=info
logging.level.jp.co.ppih.taxrefund=debug
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{hashId}] [%thread] [%-5level] %c.%M:%L - %msg%n
logging.charset.console=UTF-8
#\u914D\u7F6E\u65E5\u5FD7\u6587\u4EF6\u683C\u5F0F
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{hashId}] [%thread] [%-5level] %c.%M:%L - %msg%n
# \u65E5\u5FD7\u6587\u4EF6\u6700\u5927\u5927\u5C0F
logging.logback.rollingpolicy.max-file-size=10GB
logging.file.path=/opt/logs/${spring.application.name}/
logging.file.name=${logging.file.path}${spring.application.name}.log
# jpa
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

View File

@ -0,0 +1,3 @@
Bootstrap v5.3.7
替换了主题色(primary)
#0d6efd -> #337ab7

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.7 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #337ab7;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #337ab7;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #337ab7;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
line-height: inherit;
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.7 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #337ab7;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #337ab7;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #337ab7;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
line-height: inherit;
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,102 @@
<!-- templates/view/permission-manage.html -->
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>菜单管理</title>
</head>
<body>
<div th:fragment="content">
<h2>菜单管理</h2>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">菜单列表</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>菜单名称</th>
<th>菜单类型</th>
<th>资源值</th>
<th>上级菜单</th>
<th>排序</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>用户管理</td>
<td>1(MENU)</td>
<td></td>
<td></td>
<td>1</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
<tr>
<td>2</td>
<td>用户管理</td>
<td>2(PATH)</td>
<td>user/user-manage</td>
<td>1</td>
<td>1</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
<tr>
<td>3</td>
<td>用户管理</td>
<td>3(BTN)</td>
<td>create</td>
<td>2</td>
<td>1</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
<tr>
<td>4</td>
<td>用户管理</td>
<td>3(BTN)</td>
<td>update</td>
<td>2</td>
<td>2</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
<tr>
<td>5</td>
<td>用户管理</td>
<td>3(BTN)</td>
<td>delete</td>
<td>2</td>
<td>3</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>添加用户</title>
<!-- Bootstrap 5 CSS -->
<link href="../../static/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css"
th:href="@{/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css}"
rel="stylesheet">
<!-- jQuery -->
<script src="../../static/plugin/jquery-3.7.1/jquery-3.7.1.min.js"
th:src="@{/plugin/jquery-3.7.1/jquery-3.7.1.min.js}"></script>
<script src="../../static/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js"
th:src="@{/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js}"></script>
</head>
<body th:fragment="content">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addRoleModalLabel">添加角色</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addRoleForm">
<div class="mb-3">
<label for="addRoleName" class="form-label">角色名称</label>
<input type="text" class="form-control" id="addRoleName" name="name" required>
</div>
<div class="mb-3">
<label for="addRoleLevel" class="form-label">角色级别</label>
<input type="number" class="form-control" id="addRoleLevel" name="level" required>
<div class="form-text">数值越小权限越高</div>
</div>
<div class="mb-3">
<label for="addRoleDescription" class="form-label">角色描述</label>
<textarea class="form-control" id="addRoleDescription" name="description" rows="3"></textarea>
</div>
<!-- 菜单权限选择 -->
<div class="mb-3">
<label class="form-label">菜单权限</label>
<div id="menuTreeContainer" class="border p-3 rounded">
<div id="menuTree"></div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveBtn">保存</button>
</div>
</div>
<script>
// 加载菜单树
function addRoleInit() {
$.ajax({
url: '/menu/list',
type: 'get',
dataType: 'json',
success: function (res) {
if (res.code === 200 && res.data) {
renderMenuTree(res.data);
}
},
error: function (xhr, status, error) {
console.error('加载菜单失败:', error);
alert('加载菜单列表失败');
}
});
}
// 渲染带复选框的菜单树
function renderMenuTree(menus) {
const menuTree = $('#menuTree');
menuTree.empty();
if (!menus || menus.length === 0) {
menuTree.html('<p class="text-muted">暂无菜单数据</p>');
return;
}
const treeHtml = buildMenuTreeWithCheckbox(menus);
menuTree.html(treeHtml);
// 绑定复选框事件
bindCheckboxEvents();
}
// 构建带复选框的菜单树HTML
function buildMenuTreeWithCheckbox(menus, level = 0, parentIds = []) {
let html = '<ul class="list-group">';
menus.forEach(function(menu) {
const hasChildren = menu.children && menu.children.length > 0;
const paddingLeft = level * 20;
const currentIds = [...parentIds, menu.id];
const uniqueId = 'menu_' + currentIds.join('_');
html += '<li class="list-group-item border-0" style="padding-left: ' + paddingLeft + 'px;">';
html += '<div class="form-check">';
html += '<input class="form-check-input menu-checkbox" type="checkbox" value="' + menu.id + '" id="' + uniqueId + '" data-menu-id="' + menu.id + '">';
html += '<label class="form-check-label d-flex align-items-center" for="' + uniqueId + '">';
html += '<span class="me-2">';
html += hasChildren ? '📂' : '📄';
html += '</span>';
html += '<span>' + menu.name + '</span>';
html += '</label>';
html += '</div>';
if (hasChildren) {
html += buildMenuTreeWithCheckbox(menu.children, level + 1, currentIds);
}
html += '</li>';
});
html += '</ul>';
return html;
}
// 绑定复选框事件
function bindCheckboxEvents() {
// 父级选中时,子级也选中;父级取消时,子级也取消
$('.menu-checkbox').change(function() {
const isChecked = $(this).prop('checked');
const menuId = $(this).data('menu-id');
// 查找所有子级复选框并设置相同状态
$(this).closest('li').find('ul .menu-checkbox').prop('checked', isChecked);
// 如果选中,向上查找父级并选中
if (isChecked) {
$(this).parents('li').find('> div > .menu-checkbox:first').prop('checked', true);
}
});
}
// 获取选中的菜单ID列表
function getSelectedMenuIds() {
const selectedIds = [];
$('.menu-checkbox:checked').each(function() {
selectedIds.push($(this).val());
});
return selectedIds;
}
$(function () {
// 保存角色
$('#saveRoleBtn').click(function() {
const formData = {
name: $('#addRoleName').val(),
level: $('#addRoleLevel').val(),
description: $('#addRoleDescription').val()
};
$.ajax({
url: '/role/add',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(formData),
success: function(res) {
if (res.code === 200) {
alert('角色添加成功');
$('#addRoleModal').modal('hide');
// 刷新角色列表
location.reload();
} else {
alert('角色添加失败: ' + res.message);
}
},
error: function(xhr, status, error) {
console.error('添加角色失败:', error);
alert('添加角色失败');
}
});
});
})
function addRoleInit() {
$.ajax({
url: '/menu/list',
type: 'get',
dataType: 'json',
success: function (res) {
var roleSelect = $('#addRoleId');
roleSelect.empty();
roleSelect.append('<option value="">请选择角色</option>');
if (res.data && res.data.length > 0) {
$.each(res.data, function (index, role) {
roleSelect.append('<option value="' + role.id + '">' + role.name + '</option>');
});
}
},
error: function (xhr, status, error) {
console.error('加载角色失败:', error);
alert('加载角色列表失败');
}
});
}
// 获取角色菜单树
function loadRoleMenus(roleId) {
if (!roleId) {
$('#menuTreeContainer').hide();
return;
}
$.ajax({
url: '/role/' + roleId + '/menus',
type: 'get',
dataType: 'json',
success: function (res) {
if (res.return_code === "SUCCESS" && res.data) {
renderMenuTree(res.data);
$('#menuTreeContainer').show();
} else {
$('#menuTreeContainer').hide();
console.warn('未获取到菜单数据');
}
},
error: function (xhr, status, error) {
console.error('加载菜单失败:', error);
$('#menuTreeContainer').hide();
}
});
}
// 渲染菜单树
function renderMenuTree(menus) {
const menuTree = $('#menuTree');
menuTree.empty();
if (!menus || menus.length === 0) {
menuTree.html('<p class="text-muted">该角色暂无菜单权限</p>');
return;
}
const treeHtml = buildMenuTree(menus);
menuTree.html(treeHtml);
}
// 构建菜单树HTML
function buildMenuTree(menus, level = 0) {
let html = '<ul class="list-group">';
menus.forEach(function (menu) {
const hasChildren = menu.children && menu.children.length > 0;
const paddingLeft = level * 20;
html += '<li class="list-group-item border-0" style="padding-left: ' + paddingLeft + 'px;">';
html += '<span class="d-flex align-items-center">';
html += '<span class="me-2">';
html += hasChildren ? '📂' : '📄';
html += '</span>';
html += '<span>' + menu.name + '</span>';
html += '</span>';
if (hasChildren) {
html += buildMenuTree(menu.children, level + 1);
}
html += '</li>';
});
html += '</ul>';
return html;
}
// 绑定角色选择事件
$(document).on('change', '#addRoleId', function () {
const roleId = $(this).val();
loadRoleMenus(roleId);
});
</script>
</body>
</html>

View File

@ -0,0 +1,150 @@
<!-- templates/view/role-manage.html -->
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>角色管理</title>
<script>
$(function (){
$.ajax({
url: '/role/list',
type: 'get',
dataType: 'json',
success: function (res) {
setData(res.data)
},
error: function (xhr, status, error) {
console.error('请求失败:', error);
}
});
// 设置表格数据
function setData(datas) {
var tbody = $('tbody');
tbody.empty(); // 清空现有数据
if (!datas || datas.length === 0) {
tbody.append('<tr><td colspan="5" class="text-center">暂无数据</td></tr>');
return;
}
// 遍历数据并添加到表格
$.each(datas, function (index, role) {
var row = '<tr>' +
'<td>' + (role.id || '') + '</td>' +
'<td>' + (role.name || '') + '</td>' +
'<td>' + (role.level || '') + '</td>' +
'<td>' + (role.description || '') + '</td>' +
'<td>' + (role.createTime || '') + '</td>' +
'<td>' +
'<button class="btn btn-sm btn-primary edit-btn" data-id="' + role.id + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger delete-btn" data-id="' + role.id + '">删除</button>' +
'</td>' +
'</tr>';
tbody.append(row);
});
// 绑定编辑和删除按钮事件
$('.edit-btn').click(function () {
var userId = $(this).data('id');
editUser(userId);
});
$('.delete-btn').click(function () {
var userId = $(this).data('id');
deleteUser(userId);
});
// 编辑用户
function editUser(userId) {
alert('编辑用户功能待实现用户ID: ' + userId);
}
// 删除用户
function deleteUser(userId) {
if (confirm('确定要删除该用户吗?')) {
alert('删除用户功能待实现用户ID: ' + userId);
}
}
}
// 打开添加用户模态框
$('#addBtn').click(function () {
addRoleInit(); // 加载角色列表
$('#addRoleForm')[0].reset(); // 重置表单
$('#addRoleModal').modal('show'); // 显示模态框
});
})
</script>
</head>
<body>
<div th:fragment="content">
<h2>角色管理</h2>
<div class="row mb-3">
<div class="col-md-6">
<div class="input-group">
<input name="username" type="text" class="form-control" placeholder="搜索角色名...">
<button class="btn btn-outline-secondary" type="button" id="searchBtn">搜索</button>
</div>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-primary" type="button" id="addBtn">添加角色</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">角色列表</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>角色名称</th>
<th>角色级别</th>
<th>角色描述</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>管理员</td>
<td>1</td>
<td>系统管理员,拥有所有权限</td>
<td>2023-01-01</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
<tr>
<td>2</td>
<td>普通用户</td>
<td>2</td>
<td>普通用户,拥有基本权限</td>
<td>2023-01-01</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="addRoleModal" tabindex="-1" aria-labelledby="addRoleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div th:include="role/role-add::content"></div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>添加用户</title>
<!-- Bootstrap 5 CSS -->
<link href="../../static/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css"
th:href="@{/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css}"
rel="stylesheet">
<!-- jQuery -->
<script src="../../static/plugin/jquery-3.7.1/jquery-3.7.1.min.js"
th:src="@{/plugin/jquery-3.7.1/jquery-3.7.1.min.js}"></script>
<script src="../../static/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js"
th:src="@{/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js}"></script>
</head>
<body th:fragment="content">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addUserModalLabel">添加用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addUserForm">
<!-- 表单字段保持不变 -->
<div class="mb-3">
<label for="addUsername" class="form-label">用户名</label>
<input type="text" class="form-control" id="addUsername" name="username" required>
</div>
<div class="mb-3">
<label for="addEmail" class="form-label">邮箱</label>
<input type="email" class="form-control" id="addEmail" name="email" required>
</div>
<div class="mb-3">
<label for="addPassword" class="form-label">密码</label>
<input type="password" class="form-control" id="addPassword" name="password" required>
</div>
<div class="mb-3">
<label for="addRoleId" class="form-label">角色</label>
<select class="form-select" id="addRoleId" name="roleId" required>
<option value="">请选择角色</option>
</select>
</div>
<!-- 添加菜单树展示区域 -->
<div class="mb-3" id="menuTreeContainer" style="display: none;">
<label class="form-label">角色菜单权限</label>
<div id="menuTree" class="border p-3 rounded"></div>
</div>
<div class="mb-3">
<label for="addStatus" class="form-label">状态</label>
<select class="form-select" id="addStatus" name="status" required>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveUserBtn">保存</button>
</div>
</div>
<script> //
function addUserInit() {
$.ajax({
url: '/role/list',
type: 'get',
dataType: 'json',
success: function (res) {
var roleSelect = $('#addRoleId');
roleSelect.empty();
roleSelect.append('<option value="">请选择角色</option>');
if (res.data && res.data.length > 0) {
$.each(res.data, function (index, role) {
roleSelect.append('<option value="' + role.id + '">' + role.name + '</option>');
});
}
},
error: function (xhr, status, error) {
console.error('加载角色失败:', error);
alert('加载角色列表失败');
}
});
}
// 获取角色菜单树
function loadRoleMenus(roleId) {
if (!roleId) {
$('#menuTreeContainer').hide();
return;
}
$.ajax({
url: '/role/' + roleId + '/menus',
type: 'get',
dataType: 'json',
success: function (res) {
if (res.return_code === "SUCCESS" && res.data) {
renderMenuTree(res.data);
$('#menuTreeContainer').show();
} else {
$('#menuTreeContainer').hide();
console.warn('未获取到菜单数据');
}
},
error: function (xhr, status, error) {
console.error('加载菜单失败:', error);
$('#menuTreeContainer').hide();
}
});
}
// 渲染菜单树
function renderMenuTree(menus) {
const menuTree = $('#menuTree');
menuTree.empty();
if (!menus || menus.length === 0) {
menuTree.html('<p class="text-muted">该角色暂无菜单权限</p>');
return;
}
const treeHtml = buildMenuTree(menus);
menuTree.html(treeHtml);
}
// 构建菜单树HTML
function buildMenuTree(menus, level = 0) {
let html = '<ul class="list-group">';
menus.forEach(function (menu) {
const hasChildren = menu.children && menu.children.length > 0;
const paddingLeft = level * 20;
html += '<li class="list-group-item border-0" style="padding-left: ' + paddingLeft + 'px;">';
html += '<span class="d-flex align-items-center">';
html += '<span class="me-2">';
html += hasChildren ? '📂' : '📄';
html += '</span>';
html += '<span>' + menu.name + '</span>';
html += '</span>';
if (hasChildren) {
html += buildMenuTree(menu.children, level + 1);
}
html += '</li>';
});
html += '</ul>';
return html;
}
// 绑定角色选择事件
$(document).on('change', '#addRoleId', function () {
const roleId = $(this).val();
loadRoleMenus(roleId);
});
</script>
</body>
</html>

View File

@ -0,0 +1,166 @@
<!-- templates/view/user-manage.html -->
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户管理</title>
<script>
$(function () {
$.ajax({
url: '/user/list',
type: 'get',
dataType: 'json',
success: function (res) {
setData(res.data)
},
error: function (xhr, status, error) {
console.error('请求失败:', error);
}
});
$("#searchBtn").click(function () {
var username = $('input[name = "username"]').val();
$.ajax({
url: '/user/list',
type: 'get',
dataType: 'json',
data: {
username: username,
},
success: function (res) {
setData(res.data)
},
error: function (xhr, status, error) {
console.error('请求失败:', error);
}
});
});
// 设置表格数据
function setData(datas) {
var tbody = $('tbody');
tbody.empty(); // 清空现有数据
if (!datas || datas.length === 0) {
tbody.append('<tr><td colspan="5" class="text-center">暂无数据</td></tr>');
return;
}
// 遍历数据并添加到表格
$.each(datas, function (index, user) {
var row = '<tr>' +
'<td>' + (user.username || '') + '</td>' +
'<td>' + (user.email || '') + '</td>' +
'<td>' + (user.roleName || '') + '</td>' +
'<td>' + (user.status === 1 ? '启用' : '禁用') + '</td>' +
'<td>' +
'<button class="btn btn-sm btn-primary edit-btn" data-id="' + user.id + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger delete-btn" data-id="' + user.id + '">删除</button>' +
'</td>' +
'</tr>';
tbody.append(row);
});
// 绑定编辑和删除按钮事件
$('.edit-btn').click(function () {
var userId = $(this).data('id');
editUser(userId);
});
$('.delete-btn').click(function () {
var userId = $(this).data('id');
deleteUser(userId);
});
// 编辑用户
function editUser(userId) {
alert('编辑用户功能待实现用户ID: ' + userId);
}
// 删除用户
function deleteUser(userId) {
if (confirm('确定要删除该用户吗?')) {
alert('删除用户功能待实现用户ID: ' + userId);
}
}
}
// 打开添加用户模态框
$('#addBtn').click(function () {
addUserInit(); // 加载角色列表
$('#addUserForm')[0].reset(); // 重置表单
$('#addUserModal').modal('show'); // 显示模态框
});
})
</script>
</head>
<body>
<div th:fragment="content">
<h2>用户管理</h2>
<div class="row mb-3">
<div class="col-md-6">
<div class="input-group">
<input name="username" type="text" class="form-control" placeholder="搜索用户名...">
<button class="btn btn-outline-secondary" type="button" id="searchBtn">搜索</button>
</div>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-primary" type="button" id="addBtn">添加用户</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">用户列表</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>admin</td>
<td>admin@example.com</td>
<td>管理员</td>
<td>启用</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
<tr>
<td>user</td>
<td>user@example.com</td>
<td>普通用户</td>
<td>启用</td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div th:include="user/user-add::content"></div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>首页</title>
<!-- Bootstrap 5 CSS -->
<link href="../static/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css"
th:href="@{/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css}"
rel="stylesheet">
<!-- jQuery -->
<script src="../static/plugin/jquery-3.7.1/jquery-3.7.1.min.js"
th:src="@{/plugin/jquery-3.7.1/jquery-3.7.1.min.js}"></script>
<style>
.sidebar {
width: 200px;
height: calc(100vh - 56px);
position: fixed;
top: 56px;
left: 0;
overflow-y: auto;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
}
.content {
margin-left: 200px;
margin-top: 56px;
padding: 20px;
height: calc(100vh - 56px);
overflow-y: auto;
}
.nav-link.active {
background-color: #0d6efd;
color: white !important;
}
.loading {
text-align: center;
padding: 20px;
}
/* 二级菜单样式 */
.submenu {
padding-left: 1rem;
display: none;
}
.submenu .list-group-item {
border-left: 3px solid #0d6efd;
}
.menu-has-children::after {
content: "▶";
float: right;
transition: transform 0.2s;
}
.menu-has-children.active::after {
transform: rotate(90deg);
}
.menu-has-children.active + .submenu {
display: block;
}
/* 菜单项间距调整 */
.menu-item {
cursor: pointer;
}
</style>
</head>
<body>
<!-- 顶部导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary fixed-top">
<div class="container-fluid">
<a class="navbar-brand" href="#">权限管理系统</a>
<div class="d-flex">
<span class="navbar-text text-white me-3" id="currentUser"></span>
<button class="btn btn-outline-light btn-sm" id="logoutBtn">退出</button>
</div>
</div>
</nav>
<!-- 主体内容 -->
<div class="d-flex">
<!-- 左侧菜单 -->
<div class="sidebar bg-light">
<div class="list-group list-group-flush mt-3" id="menuContainer">
<!-- 菜单将通过AJAX动态加载 -->
<div class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">菜单加载中...</span>
</div>
</div>
</div>
</div>
<!-- 右侧内容区域 -->
<div class="content flex-grow-1">
<div id="content-container">
<!-- 默认加载欢迎页内容 -->
<div class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="../static/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js"
th:src="@{/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js}"></script>
<script>
$(document).ready(function () {
// 页面加载完成后默认加载欢迎页
loadContent('/view/welcome');
// 加载菜单
loadMenu();
// 退出登录
$('#logoutBtn').on('click', function () {
window.location.href = '/logout';
});
// 获取当前用户信息(模拟)
$('#currentUser').text('当前用户: admin');
// 加载内容的函数
function loadContent(url) {
// 显示加载动画
$('#content-container').html(`
<div class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
`);
// 使用AJAX加载内容
$.ajax({
url: url,
type: 'GET',
success: function (data) {
$('#content-container').html(data);
},
error: function () {
$('#content-container').html(`
<div class="alert alert-danger">
内容加载失败,请稍后重试。
</div>
`);
}
});
}
// 加载菜单的函数
function loadMenu() {
$.ajax({
url: '/menu/list',
type: 'GET',
success: function (res) {
let menus = res.data;
if (menus.length > 0 && menus[0].id) {
renderMenu(menus);
// 存储到本地存储中
localStorage.setItem('menuTree', JSON.stringify(menus));
} else {
let menus = localStorage.getItem('menuTree');
if (menus) {
menus = JSON.parse(menus);
renderMenu(menus);
}
}
// 绑定菜单事件
bindMenuEvents();
},
error: function () {
$('#menuContainer').html(`
<div class="alert alert-danger m-3">
菜单加载失败,请稍后重试。
</div>
`);
}
});
}
// 渲染菜单函数
function renderMenu(menus) {
let menuHtml = '';
// 添加默认欢迎页
menuHtml += `
<a href="#" class="list-group-item list-group-item-action menu-item active" data-url="/view/welcome">
欢迎页
</a>
`;
// 递归渲染菜单
menus.forEach(menu => {
menuHtml += renderMenuItem(menu);
});
$('#menuContainer').html(menuHtml);
}
// 渲染单个菜单项
function renderMenuItem(menu) {
// 只显示type为1或2的菜单项
if (menu.type !== "1" && menu.type !== "2") {
return '';
}
let menuHtml = '';
if (menu.type === "1") {
// 一级菜单(有子菜单)
menuHtml += `
<a href="#" class="list-group-item list-group-item-action menu-has-children menu-item">
${menu.name}
</a>
`;
// 渲染子菜单
if (menu.children && menu.children.length > 0) {
menuHtml += '<div class="submenu">';
menu.children.forEach(child => {
menuHtml += renderMenuItem(child);
});
menuHtml += '</div>';
}
} else if (menu.type === "2") {
// 二级菜单(页面链接)
menuHtml += `
<a href="#" class="list-group-item list-group-item-action menu-item" data-url="/v/${menu.tag}">
${menu.name}
</a>
`;
}
return menuHtml;
}
// 绑定菜单事件
function bindMenuEvents() {
// 菜单点击事件(页面链接)
$('.menu-item').not('.menu-has-children').on('click', function (e) {
e.preventDefault();
// 移除所有活动状态
$('.menu-item').removeClass('active');
// 判断父元素是不是菜单,如果是菜单,则添加活动状态
if ($(this).parent().hasClass('submenu')) {
//获取当前元素的前一个兄弟元素
$(this).parent().prev().addClass('active');
}
// 为当前点击的菜单项添加活动状态
$(this).addClass('active');
// 获取目标URL并加载内容
const url = $(this).data('url');
if (url) {
loadContent(url);
}
});
// 二级菜单展开/收起 - 优化后的逻辑
$('.menu-has-children').on('click', function (e) {
e.preventDefault();
// 切换当前菜单的展开状态
$(this).toggleClass('active');
// 阻止事件冒泡,避免影响其他菜单
e.stopPropagation();
});
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录</title>
<!-- Bootstrap 5 CSS -->
<link href="../../static/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css"
th:href="@{/plugin/bootstrap-5.3.7-dist/css/bootstrap.min.css}"
rel="stylesheet">
<!-- jQuery -->
<script src="../../static/plugin/jquery-3.7.1/jquery-3.7.1.min.js"
th:src="@{/plugin/jquery-3.7.1/jquery-3.7.1.min.js}"></script>
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-4 col-md-6 col-sm-8">
<div class="card mt-5 shadow">
<div class="card-header text-center">
<h3>用户登录</h3>
</div>
<div class="card-body">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" placeholder="请输入用户名">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" placeholder="请输入密码">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="login">登录</button>
</div>
</form>
<div id="message" class="mt-3"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="../../static/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js"
th:src="@{/plugin/bootstrap-5.3.7-dist/js/bootstrap.bundle.min.js}"></script>
<script>
$(document).ready(function () {
$('#loginForm').on('submit', function (e) {
e.preventDefault(); // 阻止表单默认提交
// 获取表单数据
var username = $('#username').val();
var password = $('#password').val();
// 简单验证
if (!username || !password) {
$('#message').html('<div class="alert alert-warning">请输入用户名和密码</div>');
return;
}
// Ajax提交
$.ajax({
url: '/login', // 根据实际接口地址修改
type: 'POST',
data: {
username: username,
password: password
},
dataType: 'json',
beforeSend: function () {
$('#login').prop('disabled', true).text('登录中...');
},
success: function (response) {
if (response.return_code === "SUCCESS") {
$('#message').html('<div class="alert alert-success">登录成功,正在跳转...</div>');
// 登录成功后的处理,比如跳转页面
setTimeout(function () {
window.location.href = response.redirectUrl || '/view/index'; // 根据实际跳转地址修改
}, 1000);
} else {
$('#message').html('<div class="alert alert-danger">' + response.return_msg + '</div>');
}
},
error: function () {
$('#message').html('<div class="alert alert-danger">登录请求失败,请稍后重试</div>');
},
complete: function () {
$('#login').prop('disabled', false).text('登录');
}
});
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!-- templates/view/welcome.html -->
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>欢迎页</title>
</head>
<body>
<div th:fragment="content">
<h2>欢迎使用权限管理系统</h2>
<p>请选择左侧菜单进行操作</p>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">系统信息</h5>
<p class="card-text">当前系统运行正常</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">快捷操作</h5>
<p class="card-text">您可以从左侧菜单开始操作</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
package top.yexuejc.admin;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class PermissionRoleManagementSystemApplicationTests {
@Test
void contextLoads() {
}
}