32 Commits

Author SHA1 Message Date
be30f31990 【优化】拦截地址 2025-10-31 10:13:52 +08:00
ac4d7d8176 【改进】菜单动态化 2025-10-31 10:13:34 +08:00
54997d4e3d 【新增】实体基类 2025-10-31 10:13:13 +08:00
7bbb1be2a2 【新增】首页样式封装 2025-10-31 10:12:31 +08:00
19312e7a6f 【新增】系统菜单 2025-10-31 10:12:00 +08:00
727dd254f0 【新增】封装axios 2025-10-31 10:11:33 +08:00
2942fdabfe 【优化】优化政策协议接口 2025-10-30 16:05:23 +08:00
5ea30d6f85 【新增】排除隐私政策接口拦截 2025-10-30 12:29:30 +08:00
e6ae7d60e3 【新增】隐私政策 2025-10-30 12:29:03 +08:00
cca1847fbf 【优化】路径 2025-10-30 10:27:52 +08:00
10a317bed6 【新增】接口注释 2025-10-30 10:20:39 +08:00
a3bf673440 Merge branch 'main' of http://120.24.204.180:3000/admin/core_wing_web 2025-10-30 09:55:37 +08:00
30c4ec612a Merge remote-tracking branch 'origin/main' 2025-10-30 09:50:14 +08:00
486455af81 Merge branch 'dev_20251030' 2025-10-30 09:49:24 +08:00
e8dc76ef82 【新增】新增显示教程详情
Some checks failed
CI Build and Test / build (pull_request) Has been cancelled
2025-10-30 09:36:39 +08:00
8aa6fc0abf 【优化】拦截地址 2025-10-30 09:36:09 +08:00
7eda1a85eb 【优化】使用Thymeleaf渲染 2025-10-30 09:35:46 +08:00
6a124a5754 【删除】教程展示测试文件 2025-10-30 09:34:03 +08:00
564e617802 【改进】模块化 2025-10-30 09:32:55 +08:00
3f09aa1238 【新增】模板引擎支持包 2025-10-30 09:31:24 +08:00
b6cdbba20a Merge pull request 'dev_20251029' (#3) from dev_20251029 into main
Reviewed-on: #3
2025-10-29 10:30:41 +00:00
bc77cd244b Merge pull request #5
dev_20251029
2025-10-29 18:26:53 +08:00
5306431417 Merge pull request #4
dev_20251029
2025-10-29 18:15:44 +08:00
53287f0a1d Merge pull request #3
dev_20251029
2025-10-29 18:12:57 +08:00
efbe2a3def Merge pull request 'dev_20251029' (#2) from dev_20251029 into main
Reviewed-on: #2
2025-10-29 08:59:47 +00:00
a1cbb9fdf0 Merge pull request #2 from MakeSomeFakeNews/dev_20251029
dev_20251029
2025-10-29 16:57:06 +08:00
a84e8b9fe6 Merge pull request '【优化】优化自增id' (#1) from dev_20251028 into main
Reviewed-on: #1
2025-10-28 08:39:06 +00:00
98052c0ce7 删除 .gitea/workflows/deploy.yml 2025-10-28 07:44:22 +00:00
3780b9d2ab 删除 .gitea/workflows/ci.yml
Some checks failed
Deploy to Server / build-and-deploy (push) Has been cancelled
2025-10-28 07:44:18 +00:00
bd43d35074 删除 .gitea/workflows/README.md
Some checks failed
Deploy to Server / build-and-deploy (push) Has been cancelled
CI Build and Test / build (push) Has been cancelled
2025-10-28 07:44:03 +00:00
085cd485ad 新增后台管理
Some checks failed
CI Build and Test / build (push) Has been cancelled
Deploy to Server / build-and-deploy (push) Has been cancelled
2025-10-28 15:40:06 +08:00
9007c7de57 Merge pull request #1 from MakeSomeFakeNews/dev_20251028
Dev 20251028
2025-10-28 15:37:05 +08:00
26 changed files with 648 additions and 655 deletions

View File

@@ -1,211 +0,0 @@
# Gitea Actions 工作流配置说明
本项目配置了两个 Gitea Actions 工作流,用于自动化构建、测试和部署。
## 工作流列表
### 1. ci.yml - 持续集成
**触发时机:**
- 推送代码到 `main``master` 分支
- 创建 Pull Request
**执行内容:**
- 自动检出代码
- 配置 Java 8 环境
- 使用 Gradle 构建项目
- 运行单元测试
- 上传构建产物JAR 包)
- 上传测试报告
**查看结果:**
构建完成后,可以在 Gitea 仓库的 "Actions" 标签页查看运行结果和下载构建产物。
---
### 2. deploy.yml - 自动部署
**触发时机:**
- 推送代码到 `main``master` 分支
- 手动触发(在 Gitea Actions 页面点击 "Run workflow"
**执行内容:**
- 构建项目
- 通过 SCP 将 JAR 包上传到服务器
- 通过 SSH 重启应用
**配置步骤:**
#### 第一步:在 Gitea 中配置 Secrets
进入仓库 → Settings → Secrets添加以下密钥
| 密钥名称 | 说明 | 示例 |
|---------|------|------|
| `SERVER_HOST` | 服务器 IP 地址 | `120.24.204.180` |
| `SERVER_PORT` | SSH 端口 | `22` |
| `SERVER_USER` | SSH 用户名 | `root``ubuntu` |
| `SERVER_SSH_KEY` | SSH 私钥内容 | 完整的私钥文件内容 |
| `DEPLOY_PATH` | 部署目录路径 | `/opt/corewing` |
#### 第二步:生成 SSH 密钥对(如果还没有)
```bash
# 在本地生成密钥对
ssh-keygen -t rsa -b 4096 -C "gitea-deploy" -f ~/.ssh/gitea_deploy
# 查看公钥
cat ~/.ssh/gitea_deploy.pub
# 查看私钥(复制到 Gitea Secrets 中)
cat ~/.ssh/gitea_deploy
```
#### 第三步:配置服务器
```bash
# 1. SSH 登录到服务器
ssh user@your-server
# 2. 创建部署目录
sudo mkdir -p /opt/corewing
sudo chown $USER:$USER /opt/corewing
# 3. 添加公钥到 authorized_keys
echo "your-public-key-content" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# 4. 安装 Java如果未安装
sudo apt update
sudo apt install openjdk-8-jdk -y
java -version
```
#### 第四步:测试部署
推送代码到 main 分支,或在 Gitea Actions 页面手动触发 deploy 工作流。
---
## 启用 Gitea Actions
### Gitea 服务器端配置
如果你的 Gitea 实例还未启用 Actions需要管理员配置
1. **修改 Gitea 配置文件** (`app.ini`)
```ini
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://gitea.com
```
2. **安装 Gitea Act Runner**
```bash
# 下载 Act Runner
wget https://dl.gitea.com/act_runner/0.2.6/act_runner-0.2.6-linux-amd64
chmod +x act_runner-0.2.6-linux-amd64
mv act_runner-0.2.6-linux-amd64 /usr/local/bin/act_runner
# 注册 Runner
act_runner register --no-interactive --instance http://120.24.204.180:3000 --token YOUR_RUNNER_TOKEN
# 启动 Runner
act_runner daemon
```
3. **获取 Runner Token**
- 登录 Gitea 管理后台
- 进入 Site Administration → Actions → Runners
- 点击 "Create new Runner" 获取 Token
---
## 自定义工作流
### 修改触发条件
可以根据需要修改工作流的触发条件:
```yaml
on:
push:
branches: [ main, develop ] # 监听多个分支
tags:
- 'v*' # 监听标签
schedule:
- cron: '0 0 * * *' # 定时执行(每天午夜)
```
### 添加环境变量
在工作流中使用环境变量:
```yaml
env:
JAVA_VERSION: '8'
SPRING_PROFILES_ACTIVE: 'prod'
steps:
- name: 运行应用
run: java -jar -Dspring.profiles.active=${{ env.SPRING_PROFILES_ACTIVE }} app.jar
```
### 添加通知
可以添加构建成功/失败通知:
```yaml
- name: 发送通知
if: failure()
run: |
curl -X POST "https://your-notification-webhook" \
-d "构建失败: ${{ github.repository }}"
```
---
## 常见问题
### 1. 工作流未触发?
- 检查 Gitea Actions 是否启用
- 检查 Act Runner 是否正常运行
- 确认触发条件是否匹配
### 2. 构建失败?
- 查看 Actions 日志获取详细错误信息
- 检查 Java 版本是否正确
- 确认依赖是否能正常下载
### 3. 部署失败?
- 检查 Secrets 配置是否正确
- 测试 SSH 连接是否正常
- 检查服务器目录权限
### 4. 如何查看日志?
- Gitea 仓库 → Actions 标签页
- 点击具体的工作流运行记录
- 展开各个步骤查看详细日志
---
## 最佳实践
1. **分支策略**
- `main` 分支自动部署到生产环境
- `develop` 分支自动部署到测试环境
- Pull Request 只执行构建和测试
2. **安全性**
- 不要在代码中硬编码密码和密钥
- 使用 Secrets 管理敏感信息
- 定期轮换 SSH 密钥
3. **性能优化**
- 启用 Gradle 缓存加快构建速度
- 使用 `build -x test` 跳过测试快速构建
- 合理设置产物保留时间
4. **监控**
- 定期检查工作流运行状态
- 设置构建失败通知
- 保存构建日志便于问题排查
---
## 更多资源
- [Gitea Actions 官方文档](https://docs.gitea.io/en-us/actions/)
- [GitHub Actions 语法参考](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions)Gitea Actions 兼容 GitHub Actions 语法)
- [Act Runner 项目](https://gitea.com/gitea/act_runner)

View File

@@ -1,55 +0,0 @@
name: CI Build and Test
# 触发条件:推送到 main 分支或创建 Pull Request
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. 检出代码
- name: 检出代码
uses: actions/checkout@v3
# 2. 设置 Java 环境
- name: 设置 Java 8
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'temurin'
cache: 'gradle'
# 3. 赋予 Gradle wrapper 执行权限
- name: 赋予 Gradle wrapper 执行权限
run: chmod +x gradlew
# 4. 构建项目(跳过测试)
- name: 使用 Gradle 构建项目
run: ./gradlew build -x test
# 5. 运行测试
- name: 运行测试
run: ./gradlew test
# 6. 上传构建产物JAR 文件)
- name: 上传 JAR 包
uses: actions/upload-artifact@v3
if: success()
with:
name: corewing-app
path: build/libs/*.jar
retention-days: 7
# 7. 上传测试报告
- name: 上传测试报告
uses: actions/upload-artifact@v3
if: always()
with:
name: test-reports
path: build/reports/tests/
retention-days: 7

View File

@@ -1,80 +0,0 @@
name: Deploy to Server
# 触发条件:手动触发或推送到 main 分支
on:
push:
branches: [ main, master ]
workflow_dispatch: # 允许手动触发
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# 1. 检出代码
- name: 检出代码
uses: actions/checkout@v3
# 2. 设置 Java 环境
- name: 设置 Java 8
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'temurin'
cache: 'gradle'
# 3. 赋予 Gradle wrapper 执行权限
- name: 赋予 Gradle wrapper 执行权限
run: chmod +x gradlew
# 4. 构建项目
- name: 构建项目
run: ./gradlew build -x test
# 5. 打包 JAR 文件
- name: 获取 JAR 文件名
id: jar
run: echo "jar_file=$(ls build/libs/*.jar | head -n 1)" >> $GITHUB_OUTPUT
# 6. 部署到服务器(使用 SCP
# 需要在 Gitea 仓库设置中配置以下 Secrets
# - SERVER_HOST: 服务器地址
# - SERVER_PORT: SSH 端口(默认 22
# - SERVER_USER: SSH 用户名
# - SERVER_SSH_KEY: SSH 私钥
# - DEPLOY_PATH: 部署路径(如 /opt/corewing
- name: 部署到服务器
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: "build/libs/*.jar"
target: ${{ secrets.DEPLOY_PATH }}
strip_components: 2
# 7. 重启应用(通过 SSH 执行命令)
- name: 重启应用
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
script: |
cd ${{ secrets.DEPLOY_PATH }}
# 停止旧进程
pkill -f corewing || true
# 等待进程完全停止
sleep 3
# 启动新进程
nohup java -jar *.jar > app.log 2>&1 &
# 检查启动状态
sleep 5
if pgrep -f corewing > /dev/null; then
echo "应用启动成功"
else
echo "应用启动失败"
exit 1
fi

View File

@@ -41,6 +41,7 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j' // MySQL 驱动
annotationProcessor 'org.projectlombok:lombok' // Lombok 注解处理
testImplementation 'org.springframework.boot:spring-boot-starter-test' // 测试框架
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // thymeleaf模版引擎
}
tasks.named('test') {

View File

@@ -0,0 +1,32 @@
package com.corewing.app.common.base;
import lombok.Data;
import java.util.Date;
@Data
public class BaseEntity {
/**
* 创建时间
*/
private Date createTime;
/**
* 创建人
*/
private String createBy;
/**
* 修改时间
*/
private Date updateTime;
/**
* 修改人
*/
private String updateBy;
/**
* 备注
*/
private String remark;
}

View File

@@ -29,10 +29,14 @@ public class SaTokenConfig implements WebMvcConfigurer {
.excludePathPatterns("/feedback", "/feedback/**")
// 排除教程接口(支持匿名查询)
.excludePathPatterns("/tutorial", "/tutorial/**")
// 排除隐私政策接口(支持匿名查询)
.excludePathPatterns("/agreement", "/agreement/**")
// 排除固件查询接口(不需要登录)
.excludePathPatterns("/firmware/**")
// 排除系统登录页(不需要登录)
.excludePathPatterns("/loading.html", "/admin/login.html")
// 排除静态资源
.excludePathPatterns("/", "/index.html", "/admin/login.html", "/*.css", "/*.js", "/*.ico", "/static/**")
.excludePathPatterns("/", "/*.css", "/*.js", "/*.ico", "/static/**", "/assets/**")
// 排除后台管理静态资源
.excludePathPatterns("/admin/**")
// 排除 Druid 监控

View File

@@ -0,0 +1,52 @@
package com.corewing.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("sys_menu")
public class SysMenu extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 菜单id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 菜单名称
*/
private String menuName;
/**
* 菜单地址
*/
private String menuUrl;
/**
* 菜单图标
*/
private String menuIcon;
/**
* 菜单分类
*/
private String menuCategory;
/**
* 是否隐藏
*/
private boolean visible;
}

View File

@@ -0,0 +1,9 @@
package com.corewing.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.corewing.app.entity.SysMenu;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysMenuMapper extends BaseMapper<SysMenu> {
}

View File

@@ -0,0 +1,36 @@
package com.corewing.app.modules.admin;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SysMainController {
/**
* 加载页
* @return
*/
@GetMapping({"/", "/index.html"})
public String loading() {
return "admin/loading";
}
/**
* 登录页
* @return
*/
@GetMapping("/admin/login.html")
public String login() {
return "admin/login";
}
/**
* 后台首页
* @return
*/
@GetMapping("/admin/index.html")
public String adminIndex() {
return "admin/main";
}
}

View File

@@ -0,0 +1,28 @@
package com.corewing.app.modules.admin;
import com.corewing.app.common.Result;
import com.corewing.app.entity.SysMenu;
import com.corewing.app.service.SysMenuService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.List;
@Controller
@RequestMapping("/sys/menu")
public class SysMenuController {
@Resource
private SysMenuService sysMenuService;
@GetMapping("/initSysMenu")
@ResponseBody
public Result<List<SysMenu>> initSysMenu() {
List<SysMenu> sysMenus = sysMenuService.initSysMenu();
return Result.success(sysMenus);
}
}

View File

@@ -1,4 +1,4 @@
package com.corewing.app.controller;
package com.corewing.app.modules.admin;
import cn.dev33.satoken.stp.StpUtil;
import com.corewing.app.common.Result;
@@ -32,7 +32,6 @@ public class SysUserController {
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody SysLoginRequest request, HttpServletRequest httpRequest) {
try {
// 获取登录IP
String loginIp = IpUtil.getClientIp(httpRequest);
// 执行登录

View File

@@ -0,0 +1,24 @@
package com.corewing.app.modules.app;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 隐私政策与协议
*/
@Controller
@RequestMapping("/agreement")
public class AgreementController {
/**
* 隐私政策列表
* @return
*/
@GetMapping("/viewList/{lang}")
public String viewList(@PathVariable String lang) {
return "app/agreement/index";
}
}

View File

@@ -1,4 +1,4 @@
package com.corewing.app.controller;
package com.corewing.app.modules.app;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.controller;
package com.corewing.app.modules.app;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.controller;
package com.corewing.app.modules.app;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.controller;
package com.corewing.app.modules.app;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -10,10 +10,9 @@ import com.corewing.app.service.TutorialCategoryService;
import com.corewing.app.service.TutorialService;
import com.corewing.app.util.I18nUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -21,7 +20,7 @@ import java.util.List;
* 教程接口
*/
@RequestMapping("/tutorial")
@RestController
@Controller
@Slf4j
public class TutorialController {
@@ -33,12 +32,27 @@ public class TutorialController {
this.tutorialCategoryService = tutorialCategoryService;
}
/**
* 跳转到界面查看教程详情
* @param tutorialId 教程id
* @param model 数据模型
* @return 详情页
*/
@GetMapping("/viewDetail/{tutorialId}")
public String viewDetail(@PathVariable Long tutorialId, ModelMap model) {
Tutorial tutorial = tutorialService.getById(tutorialId);
model.put("tutorial", tutorial);
return "app/tutorial/viewDetail";
}
/**
* 添加查看次数
* @param tutorialId 教程id
* @return
*/
@GetMapping("/addViewCount")
@ResponseBody
public Result<String> addViewCount(@RequestParam Long tutorialId) {
Tutorial tutorial = tutorialService.getById(tutorialId);
if(tutorial == null) {
@@ -56,6 +70,7 @@ public class TutorialController {
*
*/
@GetMapping("/category")
@ResponseBody
public Result<List<TutorialCategory>> category(
@RequestParam(required = false, defaultValue = "0") Integer firstStatus
) {
@@ -77,6 +92,7 @@ public class TutorialController {
*
*/
@GetMapping("/page")
@ResponseBody
public Result<IPage<Tutorial>> getPageList(
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size,

View File

@@ -1,4 +1,4 @@
package com.corewing.app.controller;
package com.corewing.app.modules.app;
import cn.dev33.satoken.stp.StpUtil;
import com.corewing.app.common.Result;

View File

@@ -0,0 +1,10 @@
package com.corewing.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.entity.SysMenu;
import java.util.List;
public interface SysMenuService extends IService<SysMenu> {
List<SysMenu> initSysMenu();
}

View File

@@ -0,0 +1,22 @@
package com.corewing.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.entity.SysMenu;
import com.corewing.app.mapper.SysMenuMapper;
import com.corewing.app.service.SysMenuService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
@Override
public List<SysMenu> initSysMenu() {
LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysMenu::isVisible, Boolean.TRUE);
return list(queryWrapper);
}
}

View File

@@ -0,0 +1,283 @@
:root {
--primary-color: #5B5FDE;
--secondary-color: #0EA5E9;
--accent-color: #FF6B6B;
--error-color: #F43F5E;
--success-color: #10B981;
--warning-color: #F59E0B;
--info-color: #3B82F6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #F3F4F6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 侧边栏样式 */
.sidebar {
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 0;
position: fixed;
left: 0;
top: 0;
width: 180px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 18px 12px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h4 {
font-size: 16px;
font-weight: 700;
margin: 0;
}
.sidebar-header i {
margin-right: 5px;
font-size: 16px;
}
.sidebar-menu {
padding: 12px 8px;
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.85);
padding: 9px 10px;
border-radius: 8px;
margin: 3px 0;
transition: all 0.3s;
font-weight: 500;
font-size: 13px;
display: flex;
align-items: center;
}
.sidebar .nav-link i {
width: 18px;
font-size: 15px;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.15);
color: white;
transform: translateX(5px);
}
.sidebar .nav-link.active {
background-color: rgba(255, 255, 255, 0.25);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 12px 8px;
}
.btn-logout {
width: 100%;
padding: 9px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
border-radius: 8px;
font-weight: 600;
font-size: 13px;
transition: all 0.3s;
}
.btn-logout:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
}
/* 顶部导航栏 */
.navbar {
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 12px 25px;
}
.navbar-brand {
font-size: 18px;
font-weight: 700;
color: #1F2937;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: #4B5563;
font-weight: 500;
font-size: 14px;
}
.user-info i {
font-size: 20px;
color: var(--primary-color);
}
/* 主内容区 */
.main-content {
margin-left: 180px;
}
/* 内容区域 */
.content-wrapper {
padding: 25px;
}
/* 欢迎卡片 */
.welcome-card {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border-radius: 16px;
padding: 35px;
margin-bottom: 25px;
box-shadow: 0 10px 30px rgba(91, 95, 222, 0.2);
position: relative;
overflow: hidden;
}
.welcome-card::before {
content: '';
position: absolute;
width: 250px;
height: 250px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
top: -80px;
right: -80px;
}
.welcome-card h2 {
font-size: 26px;
font-weight: 700;
margin-bottom: 10px;
position: relative;
z-index: 1;
}
.welcome-card p {
font-size: 15px;
opacity: 0.95;
position: relative;
z-index: 1;
}
/* 统计卡片 */
.stat-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
border: 1px solid #E5E7EB;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stat-card i {
font-size: 40px;
opacity: 0.9;
margin-bottom: 12px;
}
.stat-card h3 {
font-size: 30px;
font-weight: 700;
margin: 12px 0 6px;
color: #1F2937;
}
.stat-card p {
color: #6B7280;
font-size: 13px;
margin: 0;
}
.stat-card.primary i {
color: var(--primary-color);
}
.stat-card.success i {
color: var(--success-color);
}
.stat-card.warning i {
color: var(--warning-color);
}
.stat-card.info i {
color: var(--info-color);
}
/* 页面标题 */
.page-title {
font-size: 24px;
font-weight: 700;
color: #1F2937;
margin-bottom: 20px;
}
/* 卡片容器 */
.card {
background: white;
border-radius: 12px;
border: 1px solid #E5E7EB;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.card-body {
padding: 25px;
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: relative;
min-height: auto;
}
.main-content {
margin-left: 0;
}
.sidebar-footer {
position: static;
margin-top: 15px;
}
.welcome-card {
padding: 25px;
}
.welcome-card h2 {
font-size: 20px;
}
.content-wrapper {
padding: 20px;
}
}

View File

@@ -0,0 +1,83 @@
// 引入 axios@1.4.0(确保已引入 axios.min.js
if (!window.axios) throw new Error("请先引入 axios@1.4.0 脚本!");
const service = axios.create({
baseURL: "",
timeout: 5000,
headers: {
"Content-Type": "application/json;charset=utf-8"
}
});
// -------------------------- 请求拦截器:自动携带 Token --------------------------
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers["Authorization"] = token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// -------------------------- 响应拦截器:统一处理结果 --------------------------
service.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
let errorMsg = "请求失败,请稍后重试";
if (error.response) {
switch (error.response.status) {
case 401:
errorMsg = "Token 已过期,请重新登录";
localStorage.removeItem("token");
window.location.href = "/admin/login.html";
break;
case 403:
errorMsg = "暂无权限访问该接口";
break;
case 500:
errorMsg = "服务器内部错误";
break;
default:
errorMsg = error.response.data?.msg || errorMsg;
}
} else if (error.request) {
errorMsg = "网络异常,请检查网络连接";
}
console.error("接口请求错误:", errorMsg);
return Promise.reject(error);
}
);
// -------------------------- 封装常用请求方法get/post/put/delete --------------------------
const request = {
// GET
get(url, params = {}) {
return service.get(url, { params });
},
// POST
post(url, data = {}) {
return service.post(url, data);
},
// PUT
put(url, data = {}) {
return service.put(url, data);
},
// DELETE
delete(url, params = {}) {
return service.delete(url, { params });
}
};
window.request = request;

View File

@@ -80,14 +80,14 @@
</script>
</head>
<body>
<div class="container">
<div class="logo">🚀</div>
<h1>CoreWing</h1>
<p>后台管理系统</p>
<div class="loading">
<div class="spinner"></div>
<span>正在跳转...</span>
</div>
<div class="container">
<div class="logo">🚀</div>
<h1>CoreWing</h1>
<p>后台管理系统</p>
<div class="loading">
<div class="spinner"></div>
<span>正在跳转...</span>
</div>
</div>
</body>
</html>

View File

@@ -488,7 +488,7 @@
const token = localStorage.getItem('token');
if (token) {
// 如果已有 token尝试跳转到主页
// window.location.href = '/admin/index.html';
// window.location.href = '/admin/loading.html';
}
}
}).mount('#app');

View File

@@ -11,280 +11,7 @@
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--primary-color: #5B5FDE;
--secondary-color: #0EA5E9;
--accent-color: #FF6B6B;
--error-color: #F43F5E;
--success-color: #10B981;
--warning-color: #F59E0B;
--info-color: #3B82F6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #F3F4F6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 侧边栏样式 */
.sidebar {
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 0;
position: fixed;
left: 0;
top: 0;
width: 180px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 18px 12px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h4 {
font-size: 16px;
font-weight: 700;
margin: 0;
}
.sidebar-header i {
margin-right: 5px;
font-size: 16px;
}
.sidebar-menu {
padding: 12px 8px;
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.85);
padding: 9px 10px;
border-radius: 8px;
margin: 3px 0;
transition: all 0.3s;
font-weight: 500;
font-size: 13px;
display: flex;
align-items: center;
}
.sidebar .nav-link i {
width: 18px;
font-size: 15px;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.15);
color: white;
transform: translateX(5px);
}
.sidebar .nav-link.active {
background-color: rgba(255, 255, 255, 0.25);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 12px 8px;
}
.btn-logout {
width: 100%;
padding: 9px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
border-radius: 8px;
font-weight: 600;
font-size: 13px;
transition: all 0.3s;
}
.btn-logout:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
}
/* 顶部导航栏 */
.navbar {
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 12px 25px;
}
.navbar-brand {
font-size: 18px;
font-weight: 700;
color: #1F2937;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: #4B5563;
font-weight: 500;
font-size: 14px;
}
.user-info i {
font-size: 20px;
color: var(--primary-color);
}
/* 主内容区 */
.main-content {
margin-left: 180px;
}
/* 内容区域 */
.content-wrapper {
padding: 25px;
}
/* 欢迎卡片 */
.welcome-card {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border-radius: 16px;
padding: 35px;
margin-bottom: 25px;
box-shadow: 0 10px 30px rgba(91, 95, 222, 0.2);
position: relative;
overflow: hidden;
}
.welcome-card::before {
content: '';
position: absolute;
width: 250px;
height: 250px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
top: -80px;
right: -80px;
}
.welcome-card h2 {
font-size: 26px;
font-weight: 700;
margin-bottom: 10px;
position: relative;
z-index: 1;
}
.welcome-card p {
font-size: 15px;
opacity: 0.95;
position: relative;
z-index: 1;
}
/* 统计卡片 */
.stat-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
border: 1px solid #E5E7EB;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stat-card i {
font-size: 40px;
opacity: 0.9;
margin-bottom: 12px;
}
.stat-card h3 {
font-size: 30px;
font-weight: 700;
margin: 12px 0 6px;
color: #1F2937;
}
.stat-card p {
color: #6B7280;
font-size: 13px;
margin: 0;
}
.stat-card.primary i { color: var(--primary-color); }
.stat-card.success i { color: var(--success-color); }
.stat-card.warning i { color: var(--warning-color); }
.stat-card.info i { color: var(--info-color); }
/* 页面标题 */
.page-title {
font-size: 24px;
font-weight: 700;
color: #1F2937;
margin-bottom: 20px;
}
/* 卡片容器 */
.card {
background: white;
border-radius: 12px;
border: 1px solid #E5E7EB;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.card-body {
padding: 25px;
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: relative;
min-height: auto;
}
.main-content {
margin-left: 0;
}
.sidebar-footer {
position: static;
margin-top: 15px;
}
.welcome-card {
padding: 25px;
}
.welcome-card h2 {
font-size: 20px;
}
.content-wrapper {
padding: 20px;
}
}
</style>
<link rel="stylesheet" href="/assets/css/main.css">
</head>
<body>
<div id="app">
@@ -416,6 +143,10 @@
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入封装的 axiosRequest.js -->
<script src="/assets/js/axiosRequest.js"></script>
<script>
const { createApp } = Vue;
@@ -436,13 +167,9 @@
this.userInfo.username = localStorage.getItem('username') || '';
this.userInfo.realName = localStorage.getItem('realName') || '';
this.userInfo.userId = localStorage.getItem('userId') || '';
// 也可以从服务器获取最新的用户信息
const token = localStorage.getItem('token');
if (token) {
try {
axios.defaults.headers.common['satoken'] = token;
const response = await axios.get('/sys/user/info');
const response = await request.get('/sys/user/info');
if (response.data.code === 200) {
const user = response.data.data;
this.userInfo.username = user.username;
@@ -458,11 +185,7 @@
async handleLogout() {
if (confirm('确定要退出登录吗?')) {
try {
const token = localStorage.getItem('token');
if (token) {
axios.defaults.headers.common['satoken'] = token;
await axios.post('/sys/user/logout');
}
await request.get('/sys/user/logout');
} catch (error) {
console.error('退出登录失败:', error);
} finally {
@@ -480,11 +203,20 @@
// 未登录,跳转到登录页
window.location.href = '/admin/login.html';
}
},
async initMenu() {
const response = await request.get('/sys/menu/initSysMenu');
if(response.data.code === 200) {
console.log(response.data.code);
}
}
},
mounted() {
// 检查登录状态
this.checkLogin();
// 加载系统菜单
this.initMenu();
// 加载用户信息
this.loadUserInfo();
}

View File

@@ -2,11 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>教程展示</title>
<title>隐私政策</title>
</head>
<body>
打包测试推送全部分支
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${tutorial?.tutorialTitle}"></title>
</head>
<body>
</body>
</html>