Initial commit
48
.claude/1.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Your rule content
|
||||
#角色
|
||||
你是一名精通开发的高级工程师,拥有10年以上的应用开发经验,熟悉*等开发工具和技术栈。
|
||||
你的任务是帮助用户设计和开发易用且易于推护的 *** 应用。始终遵循最佳实践,并坚持干净代码和健壮架构的原则。
|
||||
|
||||
#目标
|
||||
你的目标是以用户容易理解的方式帮助他们完成“应用的设计和开发工作,确保应用功能完善、性能优异、用户体验良好。
|
||||
|
||||
#要求
|
||||
在理解用户需求、设计UI、编写代码、解决问题和项目选代优化时,你应该始终遵循以下原则:
|
||||
|
||||
|
||||
##需求理解
|
||||
-充分理解用户需求,站在用户角度思考,分析需求是否存在缺漏,并与用户讨论完善需求;
|
||||
-选择最简单的解决方案来满足用户需求,避免过度设计。
|
||||
##UI和样式设计
|
||||
-使用现代UI框架进行样式设计(例如***,这里可以根据不同开发项目仔纽展开,比如使用哪些视觉规范或者UI框架,没有的话也可以不用过多展开);
|
||||
-在不同平台上实现一致的设计和响应式模式
|
||||
##代码编写
|
||||
技术选型:根据项目需求选择合适的技术栈(例如***,这里需要仔细展开,比如介招某个技术栈用在什么地方,以及要遵循什么最佳实践)
|
||||
代码结构:强调代码的清晰性、模块化、可维护性,遵循最佳实践(如DRY原则、最小权限原则、响应式设计等)
|
||||
-代码安全性:在编写代码时,始终考虑安全性,避免引入漏洞,确保用户输入的安全处理
|
||||
-性能优化:优化代码的性能,减少资源占用,提升加载速度,确保项目的高效运行
|
||||
-测试与文档:编写单元测试,确保代码的健壮性,并提供清晰的中文注释和文档。方便后续阅读和维护
|
||||
##问题解决
|
||||
-全面阅读相关代码,理解***应用的工作原理
|
||||
-根据用户的反馈分析问题的原因,提出解决问题的思路
|
||||
-确保每次代码变更不会破坏现有功能,且尽可能保持最小的改动
|
||||
##迭代优化
|
||||
与用户保持密切沟通,根据反读调整功能和设计,确保应用符合用户需求
|
||||
在不确定需求时,主动询问用户以澄清需求或技术细节
|
||||
##方法论
|
||||
-系统2思维:以分析严谨的方式解决问题。将需求分解为更小、可管理的部分,并在实施前仔细考虑每一步
|
||||
思维树:评估多种可能的解决方案及其后果。使用结构化的方法探索不同的路径。并选择最优的解决方案
|
||||
-选代改进:在最终确定代码之前,考虑改进、边缘情况和优化。通过潜在增强的迭代,确保最终解决方案是健壮的
|
||||
|
||||
|
||||
235
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
---
|
||||
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个综合性的 **RuoYi-Vue 企业管理系统**,并集成了跨境电商 **ERP 功能**。项目主要包含:
|
||||
|
||||
* **RuoYi-Vue 核心**:基于 Spring Boot 后端和 Vue.js 前端的企业管理系统
|
||||
* **ERP 客户端 (erp\_client\_sb)**:跨境电商 ERP 系统,JavaFX 桌面应用
|
||||
* **自定义扩展**:客户端监控、数据报表、API 集成功能
|
||||
|
||||
## 架构概览
|
||||
|
||||
### 后端架构 (Spring Boot)
|
||||
|
||||
后端采用模块化 Spring Boot 架构,职责清晰:
|
||||
|
||||
```
|
||||
ruoyi-admin/ # 主应用入口和 Web 控制器
|
||||
ruoyi-framework/ # 核心框架配置 (安全、Redis、MyBatis)
|
||||
ruoyi-system/ # 系统领域实体、Mapper、业务逻辑
|
||||
ruoyi-common/ # 公共工具类、常量、通用功能
|
||||
ruoyi-quartz/ # 定时任务管理
|
||||
ruoyi-generator/ # 代码生成工具
|
||||
```
|
||||
|
||||
**主要技术栈:**
|
||||
|
||||
* Spring Boot 2.5.15 (Java 17)
|
||||
* Spring Security 5.7.12 + JWT 认证
|
||||
* MyBatis + PageHelper 分页插件
|
||||
* MySQL 数据库 + Redis 缓存与会话管理
|
||||
* Druid 1.2.23 连接池
|
||||
* Swagger 3.0.0 API 文档
|
||||
* Selenium 4.34.0 和 WebMagic 1.0.3 网页自动化
|
||||
|
||||
### 前端架构 (Vue.js)
|
||||
|
||||
前端是一个基于 Vue 2.6.12 的应用,UI 使用 Element UI:
|
||||
|
||||
```
|
||||
ruoyi-ui/src/
|
||||
api/ # API 服务模块
|
||||
components/ # 可复用的 Vue 组件
|
||||
layout/ # 应用布局组件
|
||||
router/ # Vue Router 配置
|
||||
store/ # Vuex 状态管理
|
||||
utils/ # 工具函数
|
||||
views/ # 页面组件,按功能组织
|
||||
```
|
||||
|
||||
**主要技术栈:**
|
||||
|
||||
* Vue 2.6.12 + Vue Router 3.4.9
|
||||
* Element UI 2.15.14
|
||||
* Vuex 3.6.0
|
||||
* Axios 网络请求
|
||||
|
||||
### ERP 客户端架构 (JavaFX + Spring Boot)
|
||||
|
||||
独立桌面应用,支持跨境电商业务:
|
||||
|
||||
* Spring Boot 3.5.4 + JavaFX 17.0.1 UI
|
||||
* SQLite 3.42.0 数据库 + JPA/Hibernate
|
||||
* Playwright 1.54.0 和 WebMagic 1.0.3 数据采集
|
||||
* HutoolUtils 5.8.36 工具包
|
||||
* 七牛云存储集成
|
||||
* 多平台支持(亚马逊、日本乐天、Shopee 等)
|
||||
|
||||
## 常用开发命令
|
||||
|
||||
### 后端 (Maven)
|
||||
|
||||
```bash
|
||||
# 构建打包
|
||||
mvn clean package
|
||||
|
||||
# 启动 Spring Boot 应用
|
||||
mvn spring-boot:run
|
||||
|
||||
# 从 ruoyi-admin 模块运行
|
||||
cd ruoyi-admin && mvn spring-boot:run
|
||||
|
||||
# 跳过测试打包
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# Windows 运行脚本
|
||||
ry.bat
|
||||
```
|
||||
|
||||
### 前端 (Vue.js)
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 国内镜像源安装
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动开发服务 (默认端口 80)
|
||||
npm run dev
|
||||
|
||||
# 生产环境构建
|
||||
npm run build:prod
|
||||
|
||||
# 测试环境构建
|
||||
npm run build:stage
|
||||
```
|
||||
|
||||
### ERP 客户端 (JavaFX)
|
||||
|
||||
```bash
|
||||
cd erp_client_sb
|
||||
|
||||
# 启动应用
|
||||
mvn spring-boot:run
|
||||
|
||||
# 打包可执行 JAR
|
||||
mvn clean package
|
||||
|
||||
# 使用 JavaFX 插件运行
|
||||
mvn javafx:run
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
```
|
||||
|
||||
## 核心配置说明
|
||||
|
||||
### 数据库
|
||||
|
||||
* **主库**:MySQL (Druid 连接池)
|
||||
* **Redis**:缓存、会话、分布式锁
|
||||
* **MyBatis**:XML SQL 映射,内置分页
|
||||
* **ERP 客户端**:SQLite 本地存储
|
||||
|
||||
### 安全与认证
|
||||
|
||||
* **JWT**:无状态认证,可配置过期时间
|
||||
* **Spring Security**:基于角色的访问控制
|
||||
* **CORS**:跨域通信
|
||||
* **XSS 防护**:输入过滤
|
||||
|
||||
### API 设计
|
||||
|
||||
* **RESTful 风格**:标准 HTTP 动词和状态码
|
||||
* **客户端监控接口**:ERP 客户端状态上报
|
||||
* **匿名访问**:如 `/monitor/client/api/**`
|
||||
* **统一分页**:PageHelper 插件
|
||||
* **统一响应格式**:AjaxResult 封装
|
||||
* **跨域配置**:CORS 支持
|
||||
* **Swagger 接口文档**:`/swagger-ui/index.html`
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 新功能开发
|
||||
|
||||
1. 后端:在 `ruoyi-system/domain/` 新建实体
|
||||
2. 数据库:在 `ruoyi-system/mapper/` 增加 Mapper
|
||||
3. 服务层:实现业务逻辑
|
||||
4. 控制层:在 `ruoyi-admin/web/controller/` 增加接口
|
||||
5. 前端:开发 Vue 页面和 API 模块
|
||||
6. 集成:更新路由和菜单
|
||||
|
||||
### 客户端集成功能
|
||||
|
||||
* 实时状态跟踪
|
||||
* 错误上报与日志
|
||||
* 数据采集统计
|
||||
* API 请求监控
|
||||
* 版本分发追踪
|
||||
|
||||
### 代码生成器
|
||||
|
||||
* 访问 `/tool/gen` 使用
|
||||
* 自动生成:Java 实体、Mapper、Service、Controller
|
||||
* 自动生成前端 Vue 页面和 API
|
||||
* 自动生成数据库 SQL
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 文件上传与静态资源
|
||||
|
||||
* 上传路径在 `application.yml` 中配置 (`ruoyi.profile`)
|
||||
* 支持本地存储和七牛云存储
|
||||
* 静态资源路径 `/profile/**`
|
||||
* 前端开发服务器端口:80 (可在 `vue.config.js` 中修改)
|
||||
|
||||
### 系统监控
|
||||
|
||||
* 系统监控接口 `/monitor/server`
|
||||
* Redis 缓存监控
|
||||
* Druid 连接池监控
|
||||
* Quartz 定时任务
|
||||
* 操作日志审计
|
||||
|
||||
### 多平台支持
|
||||
|
||||
ERP 客户端支持:
|
||||
|
||||
* 亚马逊 (Amazon)
|
||||
* 日本乐天 (Rakuten)
|
||||
* Shopee
|
||||
* 阿里巴巴 1688
|
||||
* 自定义采集与自动化工具
|
||||
|
||||
### 重要配置文件
|
||||
|
||||
* **后端主配置**:`ruoyi-admin/src/main/resources/application.yml`
|
||||
* **数据库配置**:`application-druid.yml`
|
||||
* **前端配置**:`ruoyi-ui/vue.config.js`
|
||||
* **Maven 配置**:根目录 `pom.xml`(父项目)和各模块 `pom.xml`
|
||||
* **ERP 客户端配置**:`erp_client_sb/src/main/resources/application.yml`
|
||||
|
||||
### 常见问题排查
|
||||
|
||||
* **端口冲突**:前端默认端口 80,可在 `vue.config.js` 修改
|
||||
* **跨域问题**:检查 `CorsConfig.java` 和 `vue.config.js` 代理配置
|
||||
* **数据库连接**:检查 `application-druid.yml` 中的数据库连接配置
|
||||
* **Maven 依赖**:使用阿里云镜像加速依赖下载
|
||||
---
|
||||
|
||||
⚠️ **额外要求**:回答时必须使用中文。
|
||||
⚠️ **代码规范要求**:代码必须遵循CLAUDE.md中的规范,代码简洁度和性能优先。
|
||||
💡 **操作提示**:在每次修改代码前,先向我说明修改的思路和方案,我确认同意后再进行代码更改。
|
||||
|
||||
---
|
||||
213
.claude/settings.local.json
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mvn clean:*)",
|
||||
"Bash(where mysql)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(mvn:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(xmllint:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(rmdir:*)",
|
||||
"Bash(rm:*)",
|
||||
"mcp__talktofigma__get_selection",
|
||||
"mcp__talktofigma__join_channel",
|
||||
"mcp__talktofigma__get_document_info",
|
||||
"mcp__talktofigma__read_my_design",
|
||||
"mcp__talktofigma__get_node_info",
|
||||
"mcp__talktofigma__scan_text_nodes",
|
||||
"mcp__talktofigma__scan_nodes_by_types",
|
||||
"mcp__talktofigma__get_nodes_info",
|
||||
"Bash(mvn compile:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(copy:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(mvn:*)",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\module-info.java\")",
|
||||
"Bash(jar tf:*)",
|
||||
"Bash(./build-simple.bat)",
|
||||
"Bash(java:*)",
|
||||
"Bash(./build-custom-installer.bat)",
|
||||
"Bash(echo $JAVA_HOME)",
|
||||
"Bash(where java)",
|
||||
"Bash(./build-installer-fixed.bat)",
|
||||
"Bash(fix-npm-install.bat)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(where mvn)",
|
||||
"Bash(./install-nsis.bat)",
|
||||
"Bash(./test-final.bat)",
|
||||
"Bash(./build-final-installer.bat)",
|
||||
"Bash(npm install)",
|
||||
"Bash(./快速启动-ERP客户端.bat)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js\\table-scroll-fix.js\")",
|
||||
"mcp__context7-mcp__resolve-library-id",
|
||||
"WebSearch",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(move:*)",
|
||||
"Bash(jar:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\test\\SeleniumBrowserTest.java\")",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\RakutenCacheService.java\")",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\CacheService.java\")",
|
||||
"Bash(git checkout:*)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views\\monitor\\key/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\java\\com\\ruoyi\\system\\domain/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\resources\\mapper\\system/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-common\\src\\main\\java\\com\\ruoyi\\common\\core\\redis/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\html/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\java\\com\\ruoyi\\system\\domain/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-system\\src\\main\\resources\\mapper\\system/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\util/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\utils/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-ui\\src\\views/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\utils/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb\\src\\main\\resources\\static\\js/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\erp_client_sb/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\controller\\monitor/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(/C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\ruoyi-admin\\src\\main\\java\\com\\ruoyi\\web\\service\\impl/**)",
|
||||
"Read(//c/Users/ZiJIe/Desktop/MongooCrawler-feature-monitor/**)",
|
||||
"Bash(wmic process where:*)",
|
||||
"Bash(del:*)",
|
||||
"Bash(ren:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(mysql:*)",
|
||||
"Bash(rd:*)",
|
||||
"Bash(git clean:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(./fix-electron.bat)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(tsc)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(./create-dev-version.bat)",
|
||||
"Bash(./prepare-backend.bat)",
|
||||
"Bash(./start-erp.bat)",
|
||||
"Bash(npm config set:*)",
|
||||
"Bash(set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/)",
|
||||
"Bash(npm cache clean:*)",
|
||||
"Bash(npx vite:*)",
|
||||
"Bash(./dev-ruoyi-erp.bat)",
|
||||
"Bash(./start-desktop-app.bat)",
|
||||
"Bash(cnpm install:*)",
|
||||
"Bash(cnpm uninstall:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": [],
|
||||
"additionalDirectories": [
|
||||
"C:\\c\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue",
|
||||
"C:\\Users\\ZiJIe\\Desktop\\wox",
|
||||
"C:\\c\\Users\\ZiJIe"
|
||||
]
|
||||
},
|
||||
"dangerouslySkipPermissions": true
|
||||
}
|
||||
24
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="ruoyi-framework" />
|
||||
<module name="ruoyi-system" />
|
||||
<module name="ruoyi-common" />
|
||||
<module name="ruoyi-generator" />
|
||||
<module name="erp_client_sb" />
|
||||
<module name="ruoyi-quartz" />
|
||||
<module name="ruoyi-admin" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="erp_client_sb" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/erp_client_sb/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-admin/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-admin/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-common/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-common/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-framework/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-framework/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-generator/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-generator/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-quartz/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-quartz/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-system/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/ruoyi-system/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
35
.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="public" />
|
||||
<option name="name" value="aliyun nexus" />
|
||||
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="aliyun-repository" />
|
||||
<option name="name" value="aliyun repository" />
|
||||
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss-repository" />
|
||||
<option name="name" value="jboss repository" />
|
||||
<option name="url" value="http://repository.jboss.org/nexus/content/groups/public-jboss/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
<option value="$PROJECT_DIR$/erp_client_sb/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
111
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="71d19dd6-7472-4ebf-b309-b7afee3f99de" name="更改" comment="1" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="332JslhtSnNRRZRMrLiHaPZ3q2S" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="autoscrollFromSource" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RunManager" selected="Spring Boot.RuoYiApplication">
|
||||
<configuration default="true" type="JetRunConfigurationType">
|
||||
<module name="RuoYi-Vue" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
|
||||
<module name="RuoYi-Vue" />
|
||||
<option name="filePath" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="ErpClientSbApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<module name="erp_client_sb" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.tashow.erp.ErpClientSbApplication" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="RuoYiApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<module name="ruoyi-admin" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.ruoyi.RuoYiApplication" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.23774.435" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-f27c65a3e318-JavaScript-IU-251.23774.435" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="默认任务">
|
||||
<changelist id="71d19dd6-7472-4ebf-b309-b7afee3f99de" name="更改" comment="" />
|
||||
<created>1758509443816</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1758509443816</updated>
|
||||
<workItem from="1758509446082" duration="2966000" />
|
||||
<workItem from="1758512571699" duration="311000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758511833782</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758511833782</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758512348322</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758512348322</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="3" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="1" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="1" />
|
||||
</component>
|
||||
</project>
|
||||
211
CLAUDE.md
Normal file
@@ -0,0 +1,211 @@
|
||||
|
||||
---
|
||||
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 **RuoYi-Vue 3.9.0** 的企业管理系统,集成了跨境电商 ERP 功能。项目包含:
|
||||
|
||||
* **RuoYi-Vue 核心**:基于 Spring Boot 2.5.15 后端和 Vue.js 2.6.12 前端的企业管理平台
|
||||
* **ERP 客户端 (erp_client_sb)**:独立的跨境电商 ERP 桌面应用 (JavaFX + Spring Boot 3.5.4)
|
||||
* **客户端监控扩展**:实时监控、数据报表、API 集成等自定义功能
|
||||
|
||||
## 项目架构
|
||||
|
||||
### 主项目模块结构 (Maven 多模块)
|
||||
|
||||
```
|
||||
ruoyi-admin/ # 主应用入口,Web 控制器层
|
||||
ruoyi-framework/ # 核心框架配置 (Spring Security, Redis, MyBatis)
|
||||
ruoyi-system/ # 系统核心模块 (实体类、Mapper、Service)
|
||||
ruoyi-common/ # 公共工具类和常量定义
|
||||
ruoyi-quartz/ # 定时任务管理模块
|
||||
ruoyi-generator/ # 代码生成器模块
|
||||
ruoyi-ui/ # Vue.js 前端应用
|
||||
erp_client_sb/ # 独立的跨境电商 ERP 客户端
|
||||
```
|
||||
|
||||
### 技术栈详情
|
||||
|
||||
**后端 (Spring Boot 2.5.15)**
|
||||
- Java 17 + Maven 3.11.0
|
||||
- Spring Security 5.7.12 + JWT 0.9.1 认证
|
||||
- MyBatis + PageHelper 1.4.7 分页
|
||||
- MySQL + Redis 缓存
|
||||
- Druid 1.2.23 连接池
|
||||
- Swagger 3.0.0 API 文档
|
||||
- FastJSON 2.0.57 JSON 处理
|
||||
|
||||
**前端 (Vue.js 2.6.12)**
|
||||
- Element UI 2.15.14 组件库
|
||||
- Vue Router 3.4.9 + Vuex 3.6.0
|
||||
- Axios 0.28.1 HTTP 客户端
|
||||
- ECharts 5.4.0 图表库
|
||||
- Webpack 构建 (vue-cli-service 4.4.6)
|
||||
|
||||
**ERP 客户端 (JavaFX + Spring Boot 3.5.4)**
|
||||
- OpenJFX 17.0.1 桌面 UI
|
||||
- SQLite 3.42.0 本地数据库 + JPA/Hibernate
|
||||
- WebMagic 1.0.3 + Selenium 4.23.0 网页爬取
|
||||
- HutoolUtils 5.8.36 工具库
|
||||
- 七牛云 7.12.1 存储服务
|
||||
|
||||
## 常用开发命令
|
||||
|
||||
### 后端开发 (Maven)
|
||||
|
||||
```bash
|
||||
# 启动主应用 (推荐)
|
||||
cd ruoyi-admin && mvn spring-boot:run
|
||||
|
||||
# 从根目录启动
|
||||
mvn spring-boot:run -pl ruoyi-admin
|
||||
|
||||
# 打包部署
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 编译项目
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
### 前端开发 (npm)
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
|
||||
# 安装依赖 (建议使用国内镜像)
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动开发服务器 (端口 80)
|
||||
npm run dev
|
||||
|
||||
# 生产环境构建
|
||||
npm run build:prod
|
||||
|
||||
# 测试环境构建
|
||||
npm run build:stage
|
||||
```
|
||||
|
||||
### ERP 客户端开发
|
||||
|
||||
```bash
|
||||
cd erp_client_sb
|
||||
|
||||
# 启动 JavaFX 应用
|
||||
mvn spring-boot:run
|
||||
# 或者
|
||||
mvn javafx:run
|
||||
|
||||
# 打包可执行 JAR
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
## 核心配置与架构要点
|
||||
|
||||
### Maven 依赖管理
|
||||
|
||||
- 使用阿里云 Maven 镜像 (maven.aliyun.com)
|
||||
- 父 POM 统一管理版本号和依赖
|
||||
- 安全版本覆盖:Tomcat 9.0.106, Logback 1.2.13, Spring Framework 5.3.39
|
||||
|
||||
### 前端开发配置
|
||||
|
||||
- **开发服务器**:端口 80,支持热重载
|
||||
- **代理配置**:`vue.config.js` 中配置后端 API 代理 (`http://8.138.23.49:8080`)
|
||||
- **构建优化**:Gzip 压缩、代码分割、Element UI 单独打包
|
||||
|
||||
### 数据库与缓存
|
||||
|
||||
- **MySQL**:主数据库,通过 Druid 连接池管理
|
||||
- **Redis**:会话存储、缓存、分布式锁
|
||||
- **SQLite**:ERP 客户端本地数据存储
|
||||
|
||||
### 安全与认证
|
||||
|
||||
- **JWT 无状态认证**:支持多终端
|
||||
- **RBAC 权限模型**:角色-菜单-按钮权限
|
||||
- **跨域配置**:支持前后端分离部署
|
||||
- **XSS 防护**:输入过滤和输出编码
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### 新功能开发标准流程
|
||||
|
||||
1. **后端开发**:
|
||||
- 在 `ruoyi-system/src/main/java/com/ruoyi/system/domain/` 创建实体类
|
||||
- 在 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/` 创建 Mapper 接口
|
||||
- 在 `ruoyi-system/src/main/resources/mapper/system/` 创建 MyBatis XML
|
||||
- 在 `ruoyi-system/src/main/java/com/ruoyi/system/service/` 实现业务逻辑
|
||||
- 在 `ruoyi-admin/src/main/java/com/ruoyi/web/controller/` 创建 REST 控制器
|
||||
|
||||
2. **前端开发**:
|
||||
- 在 `ruoyi-ui/src/api/` 创建 API 服务模块
|
||||
- 在 `ruoyi-ui/src/views/` 创建页面组件
|
||||
- 更新路由配置和菜单权限
|
||||
|
||||
3. **代码生成器**:
|
||||
- 访问 `/tool/gen` 快速生成 CRUD 代码
|
||||
- 支持 Java、Vue、SQL 代码自动生成
|
||||
|
||||
### 客户端集成开发
|
||||
|
||||
ERP 客户端提供的核心功能:
|
||||
- 多平台数据采集 (Amazon, Rakuten, Shopee, 1688)
|
||||
- 实时状态监控与错误上报
|
||||
- 与主系统的 API 集成 (`/monitor/client/api/**`)
|
||||
|
||||
## 重要配置文件
|
||||
|
||||
### 后端配置
|
||||
- **主配置**:`ruoyi-admin/src/main/resources/application.yml`
|
||||
- **数据源配置**:`ruoyi-admin/src/main/resources/application-druid.yml`
|
||||
- **Maven 配置**:根目录 `pom.xml` (父 POM)
|
||||
|
||||
### 前端配置
|
||||
- **构建配置**:`ruoyi-ui/vue.config.js`
|
||||
- **包管理**:`ruoyi-ui/package.json`
|
||||
|
||||
### ERP 客户端配置
|
||||
- **应用配置**:`erp_client_sb/src/main/resources/application.yml`
|
||||
- **Maven 配置**:`erp_client_sb/pom.xml`
|
||||
|
||||
## 系统监控与工具
|
||||
|
||||
### 内置监控功能
|
||||
- 系统性能监控:`/monitor/server`
|
||||
- Redis 缓存监控:缓存信息查询和命令统计
|
||||
- Druid 连接池监控:SQL 性能分析
|
||||
- 在线用户监控:当前活跃用户状态
|
||||
- 操作日志:系统操作记录和异常日志
|
||||
|
||||
### API 文档
|
||||
- Swagger UI:`/swagger-ui/index.html`
|
||||
- 自动生成的接口文档
|
||||
|
||||
## 部署和环境
|
||||
|
||||
### 开发环境要求
|
||||
- **Java**:JDK 17
|
||||
- **Node.js**:>= 8.9
|
||||
- **npm**:>= 3.0.0
|
||||
- **MySQL**:5.7+ 或 8.0+
|
||||
- **Redis**:6.0+
|
||||
|
||||
### 常见问题排查
|
||||
- **端口冲突**:前端默认 80 端口,可在 `vue.config.js` 修改
|
||||
- **跨域问题**:检查 `vue.config.js` 代理配置和后端 CORS 设置
|
||||
- **Maven 依赖**:已配置阿里云镜像加速
|
||||
- **JavaFX 运行**:确保 JDK 包含 JavaFX 模块或单独安装 OpenJFX
|
||||
---
|
||||
|
||||
⚠️ **额外要求**:回答时必须使用中文。
|
||||
💡 **操作提示**:在每次修改代码前,我会先向您说明修改的思路和方案,请您确认同意后再进行代码更改。
|
||||
|
||||
|
||||
---
|
||||
95
README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.0</h1>
|
||||
<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4>
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
|
||||
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.0-brightgreen.svg"></a>
|
||||
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
||||
</p>
|
||||
|
||||
## 平台简介
|
||||
|
||||
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
|
||||
* 前端采用Vue、Element UI。
|
||||
* 后端采用Spring Boot、Spring Security、Redis & Jwt。
|
||||
* 权限认证使用Jwt,支持多终端认证系统。
|
||||
* 支持加载动态权限菜单,多方式轻松权限控制。
|
||||
* 高效率开发,使用代码生成器可以一键生成前后端代码。
|
||||
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3),保持同步更新。
|
||||
* 提供了单应用版本[RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast),Oracle版本[RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。
|
||||
* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)
|
||||
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
|
||||
3. 岗位管理:配置系统用户所属担任职务。
|
||||
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
11. 在线用户:当前系统中活跃用户状态监控。
|
||||
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
|
||||
14. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
16. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
17. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||
18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
|
||||
|
||||
## 在线体验
|
||||
|
||||
- admin/admin123
|
||||
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
|
||||
|
||||
演示地址:http://vue.ruoyi.vip
|
||||
文档地址:http://doc.ruoyi.vip
|
||||
|
||||
## 演示图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## 若依前后端分离交流群
|
||||
|
||||
QQ群: [](https://jq.qq.com/?_wv=1027&k=5bVB1og) [](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [](https://jq.qq.com/?_wv=1027&k=51G72yr) [](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [](https://jq.qq.com/?_wv=1027&k=SpyH2875) [](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) 点击按钮入群。
|
||||
1
data/device.id
Normal file
@@ -0,0 +1 @@
|
||||
DEVICE-C705AA3904F84D998D03B5CD83EEBBD7
|
||||
BIN
data/erp-cache.db
Normal file
BIN
data/erp-cache.db-shm
Normal file
BIN
data/erp-cache.db-wal
Normal file
28
data/jwt_rsa_private.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCMgaMZlZOiAK7e
|
||||
FWjhWuyIg+8XBIrF9/kZYGtX+3tn53T+sCPBjAIukouyP41H5g6IZDeTP1WXsNuQ
|
||||
kVIvbZwRJKTSO7EiqE1T3V9jPzTXOXvq0SPnjaMhmUKxkmgRSs9DBZUqrElZ5mzw
|
||||
1ua/9ISlwCLqFRrVL5Nzc9+TO8IIRjjPBPYssYCDpz4NpdQb4OQYhaTRrj1++wo4
|
||||
N+LuS+wKb0Ps8SH1+udYaUWR6NteR6hhj0+0AVK0cyqsbHYQE4z+gAJr+Aicd1ZX
|
||||
pON8jsFiSNdTgGIVKCb8haVkgkXwe1inBwhyVcWk8xaz2aKa+TsNI03WfBi8DY39
|
||||
+WeisqnrAgMBAAECggEABJA3TF7rxxCvnT3jxKHv2bUzQDOhEDHwEK9tfROJXAQL
|
||||
7DOrTZ9u+LVAvT7MJ2Ak67AZj/o4HO+dCfJ2UV0FexcOFVfj9mSx8j3X2cDVRgIz
|
||||
cJpvSJd0i2RPYrYHFDyyQ5J8WED1NurBcgcAwo49+qYlXCXoU7EyYEcMpVsE/8C/
|
||||
xvW0GShTzXOpO5emQOkPixxNW8i6VHte23igUULn29eyePB+pvRdEuuKC+bEZ9cJ
|
||||
ZXJ0p/yS2sNUxvBAmdVZTTs88E3gu/ARRJgyHE//Ax+fHmlSpPGeJlEPivntu0So
|
||||
vrJan9uIA3mS7eE1i7M+JoZjIgwpHKfG07AipRmklQKBgQDEQAGgpMo3AtRMmYFi
|
||||
PP3Sb5p3cETekWXJBijlhE69XBZg/VBSpNrtmD4iXuYbBb3d140nIvESowpBN3gn
|
||||
uY6nNqJWSzhKzQhU8ienJS8PZPeeRE2mMI8sRxl+VnhMraa/E8k/hzqP0RCo+cfC
|
||||
uu1be7AJ0+W6WB0d9g6zSj99HQKBgQC3SOV/4i1KXybuaphJJrZF5cuMoD5cE5Ck
|
||||
vXTTc9MSkUtwLf5PiJGRzRWNd5amS0FMb3IuC8YfEqZ8k+vDDVbgigqDeIKEpJSQ
|
||||
3K8A/F/094cyMyqrQ2aiUPg6Da57QH+Q0Ab2S7n8RstZC6h28sYxgG1acuL2jqY7
|
||||
S7HlVAZ8pwKBgQC3UqMykT1kjfwLYgn+3sKsZRyCHhn3XxMZ6esiG6oCMZemGnuB
|
||||
+AWalPDV4phI/eAS71woBvfzVOIrccmIMkoT4XFb8wAuv8DcuShZdt6zHrpA2cU/
|
||||
TXUxA2nJHrVZy41MSQthkM0fs0hA0LPOMBexsaUMSSj8HXt1lXi9+sm78QKBgAsE
|
||||
0udRTa++8LQ8rFMZhLPHEOmvaJBYjMWarj9YI0Rmf8aKvVNCvp2pWrZajjAJLi/O
|
||||
M2sZQhv0HxY2PmJHlwWAxwkIYbBfxJ7A5bSFd69egj4+XT5WmwD/JS04TVkTk5e9
|
||||
Ke38t323M9pynPoptkibk/dwGL0B7nR6JIPI/WrZAoGBALYitr4UK/H8I/XvYaUJ
|
||||
q0A93fIa9ACVHshZEAwY7o1PIZu9bLScGilkbkvdATwgkfrBZXFCDIAFu2Ta/yMF
|
||||
NEFXG1szFg0aDzEN0pm8o2dndQ9dKTPswytvR5DphgCmxhuJDm9Qwwjc1UQuWePp
|
||||
Q3mDFK0eVcWWdA03UbY2TEeK
|
||||
-----END PRIVATE KEY-----
|
||||
9
data/jwt_rsa_public.pem
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjIGjGZWTogCu3hVo4Vrs
|
||||
iIPvFwSKxff5GWBrV/t7Z+d0/rAjwYwCLpKLsj+NR+YOiGQ3kz9Vl7DbkJFSL22c
|
||||
ESSk0juxIqhNU91fYz801zl76tEj542jIZlCsZJoEUrPQwWVKqxJWeZs8Nbmv/SE
|
||||
pcAi6hUa1S+Tc3PfkzvCCEY4zwT2LLGAg6c+DaXUG+DkGIWk0a49fvsKODfi7kvs
|
||||
Cm9D7PEh9frnWGlFkejbXkeoYY9PtAFStHMqrGx2EBOM/oACa/gInHdWV6TjfI7B
|
||||
YkjXU4BiFSgm/IWlZIJF8HtYpwcIclXFpPMWs9mimvk7DSNN1nwYvA2N/flnorKp
|
||||
6wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
21
electron-vue-template/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Deluze
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
77
electron-vue-template/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<div align="center">
|
||||
|
||||
# Electron Vue Template
|
||||
|
||||
<img width="794" alt="image" src="https://user-images.githubusercontent.com/32544586/222748627-ee10c9a6-70d2-4e21-b23f-001dd8ec7238.png">
|
||||
|
||||
A simple starter template for a **Vue3** + **Electron** TypeScript based application, including **ViteJS** and **Electron Builder**.
|
||||
</div>
|
||||
|
||||
## About
|
||||
|
||||
This template utilizes [ViteJS](https://vitejs.dev) for building and serving your (Vue powered) front-end process, it provides Hot Reloads (HMR) to make development fast and easy ⚡
|
||||
|
||||
Building the Electron (main) process is done with [Electron Builder](https://www.electron.build/), which makes your application easily distributable and supports cross-platform compilation 😎
|
||||
|
||||
## Getting started
|
||||
|
||||
Click the green **Use this template** button on top of the repository, and clone your own newly created repository.
|
||||
|
||||
**Or..**
|
||||
|
||||
Clone this repository: `git clone git@github.com:Deluze/electron-vue-template.git`
|
||||
|
||||
|
||||
### Install dependencies ⏬
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Start developing ⚒️
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Additional Commands
|
||||
|
||||
```bash
|
||||
npm run dev # starts application with hot reload
|
||||
npm run build # builds application, distributable files can be found in "dist" folder
|
||||
|
||||
# OR
|
||||
|
||||
npm run build:win # uses windows as build target
|
||||
npm run build:mac # uses mac as build target
|
||||
npm run build:linux # uses linux as build target
|
||||
```
|
||||
|
||||
Optional configuration options can be found in the [Electron Builder CLI docs](https://www.electron.build/cli.html).
|
||||
## Project Structure
|
||||
|
||||
```bash
|
||||
- scripts/ # all the scripts used to build or serve your application, change as you like.
|
||||
- src/
|
||||
- main/ # Main thread (Electron application source)
|
||||
- renderer/ # Renderer thread (VueJS application source)
|
||||
```
|
||||
|
||||
## Using static files
|
||||
|
||||
If you have any files that you want to copy over to the app directory after installation, you will need to add those files in your `src/main/static` directory.
|
||||
|
||||
Files in said directory are only accessible to the `main` process, similar to `src/renderer/assets` only being accessible to the `renderer` process. Besides that, the concept is the same as to what you're used to in your other front-end projects.
|
||||
|
||||
#### Referencing static files from your main process
|
||||
|
||||
```ts
|
||||
/* Assumes src/main/static/myFile.txt exists */
|
||||
|
||||
import {app} from 'electron';
|
||||
import {join} from 'path';
|
||||
import {readFileSync} from 'fs';
|
||||
|
||||
const path = join(app.getAppPath(), 'static', 'myFile.txt');
|
||||
const buffer = readFileSync(path);
|
||||
```
|
||||
39
electron-vue-template/electron-builder.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"appId": "com.electron.app",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"shortcutName": "Electron App"
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
},
|
||||
"linux": {
|
||||
"target": ["snap"]
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"from": "build/main",
|
||||
"to": "main",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "build/renderer",
|
||||
"to": "renderer",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "src/main/static",
|
||||
"to": "static",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
"!build",
|
||||
"!dist",
|
||||
"!scripts"
|
||||
]
|
||||
}
|
||||
5847
electron-vue-template/package-lock.json
generated
Normal file
33
electron-vue-template/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "electron-vue-template",
|
||||
"version": "0.1.0",
|
||||
"description": "A minimal Electron + Vue application",
|
||||
"main": "main/main.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev-server.js",
|
||||
"build": "node scripts/build.js && electron-builder",
|
||||
"build:win": "node scripts/build.js && electron-builder --win",
|
||||
"build:mac": "node scripts/build.js && electron-builder --mac",
|
||||
"build:linux": "node scripts/build.js && electron-builder --linux"
|
||||
},
|
||||
"repository": "https://github.com/deluze/electron-vue-template",
|
||||
"author": {
|
||||
"name": "Deluze",
|
||||
"url": "https://github.com/Deluze"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.4.1",
|
||||
"chalk": "^4.1.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^25.1.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"element-plus": "^2.11.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"vue": "^3.3.8"
|
||||
}
|
||||
}
|
||||
BIN
electron-vue-template/public/icon/icon.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
electron-vue-template/public/icon/img.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
electron-vue-template/public/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
electron-vue-template/public/icons/icon.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
electron-vue-template/public/image/111.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
electron-vue-template/public/image/splash_screen.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
33
electron-vue-template/public/splash.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>正在启动...</title>
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
background: #fff; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
||||
background-image: url('image/splash_screen.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
.box { position: fixed; left: 0; right: 0; bottom: 28px; padding: 0 0; }
|
||||
.progress { position: relative; width: 100vw; height: 6px; background: rgba(0,0,0,0.08); }
|
||||
.bar { position: absolute; left: 0; top: 0; height: 100%; width: 20vw; min-width: 120px; background: linear-gradient(90deg, #67C23A, #409EFF); animation: slide 1s ease-in-out infinite alternate; }
|
||||
@keyframes slide { 0% { left: 0; } 100% { left: calc(100vw - 20vw); } }
|
||||
</style>
|
||||
<link rel="icon" href="icon/icon.png">
|
||||
<link rel="apple-touch-icon" href="icon/icon.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<div class="progress"><div class="bar"></div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
32
electron-vue-template/scripts/build.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const Path = require('path');
|
||||
const Chalk = require('chalk');
|
||||
const FileSystem = require('fs');
|
||||
const Vite = require('vite');
|
||||
const compileTs = require('./private/tsc');
|
||||
|
||||
function buildRenderer() {
|
||||
return Vite.build({
|
||||
configFile: Path.join(__dirname, '..', 'vite.config.js'),
|
||||
base: './',
|
||||
mode: 'production'
|
||||
});
|
||||
}
|
||||
|
||||
function buildMain() {
|
||||
const mainPath = Path.join(__dirname, '..', 'src', 'main');
|
||||
return compileTs(mainPath);
|
||||
}
|
||||
|
||||
FileSystem.rmSync(Path.join(__dirname, '..', 'build'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
})
|
||||
|
||||
console.log(Chalk.blueBright('Transpiling renderer & main...'));
|
||||
|
||||
Promise.allSettled([
|
||||
buildRenderer(),
|
||||
buildMain(),
|
||||
]).then(() => {
|
||||
console.log(Chalk.greenBright('Renderer & main successfully transpiled! (ready to be built with electron-builder)'));
|
||||
});
|
||||
121
electron-vue-template/scripts/dev-server.js
Normal file
@@ -0,0 +1,121 @@
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
const Vite = require('vite');
|
||||
const ChildProcess = require('child_process');
|
||||
const Path = require('path');
|
||||
const Chalk = require('chalk');
|
||||
const Chokidar = require('chokidar');
|
||||
const Electron = require('electron');
|
||||
const compileTs = require('./private/tsc');
|
||||
const FileSystem = require('fs');
|
||||
const { EOL } = require('os');
|
||||
|
||||
let viteServer = null;
|
||||
let electronProcess = null;
|
||||
let electronProcessLocker = false;
|
||||
let rendererPort = 0;
|
||||
|
||||
async function startRenderer() {
|
||||
viteServer = await Vite.createServer({
|
||||
configFile: Path.join(__dirname, '..', 'vite.config.js'),
|
||||
mode: 'development',
|
||||
});
|
||||
|
||||
return viteServer.listen();
|
||||
}
|
||||
|
||||
async function startElectron() {
|
||||
if (electronProcess) { // single instance lock
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await compileTs(Path.join(__dirname, '..', 'src', 'main'));
|
||||
} catch {
|
||||
console.log(Chalk.redBright('Could not start Electron because of the above typescript error(s).'));
|
||||
electronProcessLocker = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = [
|
||||
Path.join(__dirname, '..', 'build', 'main', 'main.js'),
|
||||
rendererPort,
|
||||
];
|
||||
electronProcess = ChildProcess.spawn(Electron, args);
|
||||
electronProcessLocker = false;
|
||||
|
||||
electronProcess.stdout.on('data', data => {
|
||||
if (data == EOL) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()))
|
||||
});
|
||||
|
||||
electronProcess.stderr.on('data', data =>
|
||||
process.stderr.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()))
|
||||
);
|
||||
|
||||
electronProcess.on('exit', () => stop());
|
||||
}
|
||||
|
||||
function restartElectron() {
|
||||
if (electronProcess) {
|
||||
electronProcess.removeAllListeners('exit');
|
||||
electronProcess.kill();
|
||||
electronProcess = null;
|
||||
}
|
||||
|
||||
if (!electronProcessLocker) {
|
||||
electronProcessLocker = true;
|
||||
startElectron();
|
||||
}
|
||||
}
|
||||
|
||||
function copyStaticFiles() {
|
||||
copy('static');
|
||||
}
|
||||
|
||||
/*
|
||||
The working dir of Electron is build/main instead of src/main because of TS.
|
||||
tsc does not copy static files, so copy them over manually for dev server.
|
||||
*/
|
||||
function copy(path) {
|
||||
FileSystem.cpSync(
|
||||
Path.join(__dirname, '..', 'src', 'main', path),
|
||||
Path.join(__dirname, '..', 'build', 'main', path),
|
||||
{ recursive: true }
|
||||
);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
viteServer.close();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
async function start() {
|
||||
console.log(`${Chalk.greenBright('=======================================')}`);
|
||||
console.log(`${Chalk.greenBright('Starting Electron + Vite Dev Server...')}`);
|
||||
console.log(`${Chalk.greenBright('=======================================')}`);
|
||||
|
||||
const devServer = await startRenderer();
|
||||
rendererPort = devServer.config.server.port;
|
||||
|
||||
copyStaticFiles();
|
||||
startElectron();
|
||||
|
||||
const path = Path.join(__dirname, '..', 'src', 'main');
|
||||
Chokidar.watch(path, {
|
||||
cwd: path,
|
||||
}).on('change', (path) => {
|
||||
console.log(Chalk.blueBright(`[electron] `) + `Change in ${path}. reloading... 🚀`);
|
||||
|
||||
if (path.startsWith(Path.join('static', '/'))) {
|
||||
copy(path);
|
||||
}
|
||||
|
||||
restartElectron();
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
24
electron-vue-template/scripts/private/tsc.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const ChildProcess = require('child_process');
|
||||
const Chalk = require('chalk');
|
||||
|
||||
function compile(directory) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tscProcess = ChildProcess.exec('tsc', {
|
||||
cwd: directory,
|
||||
});
|
||||
|
||||
tscProcess.stdout.on('data', data =>
|
||||
process.stdout.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString()))
|
||||
);
|
||||
|
||||
tscProcess.on('exit', exitCode => {
|
||||
if (exitCode > 0) {
|
||||
reject(exitCode);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = compile;
|
||||
199
electron-vue-template/src/main/main.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// 主进程:创建窗口、启动后端 JAR、隐藏菜单栏
|
||||
import {app, BrowserWindow, ipcMain, session, Menu, screen} from 'electron';
|
||||
import { Socket } from 'net';
|
||||
import { existsSync } from 'fs';
|
||||
import {join, dirname} from 'path';
|
||||
import {spawn, ChildProcessWithoutNullStreams} from 'child_process';
|
||||
|
||||
// 保存后端进程与窗口引用,便于退出时清理
|
||||
let springProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let splashWindow: BrowserWindow | null = null;
|
||||
let appOpened = false;
|
||||
|
||||
function openAppIfNotOpened() {
|
||||
if (appOpened) return;
|
||||
appOpened = true;
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
if (splashWindow) { splashWindow.close(); splashWindow = null; }
|
||||
}
|
||||
|
||||
// 启动后端 Spring Boot(使用你提供的绝对路径)
|
||||
function startSpringBoot() {
|
||||
const jarPath = 'C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/ruoyi-admin/target/ruoyi-admin.jar';
|
||||
|
||||
springProcess = spawn('java', ['-jar', jarPath], {
|
||||
cwd: dirname(jarPath),
|
||||
detached: false
|
||||
});
|
||||
|
||||
// 打印后端日志,监听启动成功标志
|
||||
springProcess.stdout.on('data', (data) => {
|
||||
console.log(`SpringBoot: ${data}`);
|
||||
// 检测到启动成功日志立即进入主界面
|
||||
if (data.toString().includes('Started RuoYiApplication')) {
|
||||
openAppIfNotOpened();
|
||||
}
|
||||
});
|
||||
|
||||
// 打印后端错误,检测启动失败
|
||||
springProcess.stderr.on('data', (data) => {
|
||||
console.error(`SpringBoot ERROR: ${data}`);
|
||||
const errorStr = data.toString();
|
||||
// 检测到关键错误信息,直接退出
|
||||
if (errorStr.includes('APPLICATION FAILED TO START') ||
|
||||
errorStr.includes('Port') && errorStr.includes('already in use') ||
|
||||
errorStr.includes('Unable to start embedded Tomcat')) {
|
||||
console.error('后端启动失败,程序即将退出');
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// 后端退出时,前端同步退出
|
||||
springProcess.on('close', (code) => {
|
||||
console.log(`SpringBoot exited with code ${code}`);
|
||||
if (mainWindow) {
|
||||
mainWindow.close();
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭后端进程(Windows 使用 taskkill 结束整个进程树)
|
||||
function stopSpringBoot() {
|
||||
if (!springProcess) return;
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Force kill the whole process tree on Windows
|
||||
try {
|
||||
const pid = springProcess.pid;
|
||||
if (pid !== undefined && pid !== null) {
|
||||
spawn('taskkill', ['/pid', String(pid), '/f', '/t']);
|
||||
} else {
|
||||
springProcess.kill();
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
springProcess.kill();
|
||||
}
|
||||
} else {
|
||||
springProcess.kill('SIGTERM');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to stop Spring Boot process:', e);
|
||||
} finally {
|
||||
springProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建主窗口(预创建但隐藏)
|
||||
function createWindow () {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 彻底隐藏原生菜单栏
|
||||
try {
|
||||
Menu.setApplicationMenu(null);
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
if (typeof (mainWindow as any).setMenu === 'function') {
|
||||
(mainWindow as any).setMenu(null);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const rendererPort = process.argv[2];
|
||||
mainWindow.loadURL(`http://localhost:${rendererPort}`);
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// 预创建主窗口(隐藏)
|
||||
createWindow();
|
||||
|
||||
// 显示启动页
|
||||
const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize;
|
||||
const splashW = Math.min(Math.floor(sw * 0.8), 1800);
|
||||
const splashH = Math.min(Math.floor(sh * 0.8), 1200);
|
||||
splashWindow = new BrowserWindow({
|
||||
width: splashW,
|
||||
height: splashH,
|
||||
frame: false,
|
||||
transparent: false,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
show: true,
|
||||
center: true,
|
||||
});
|
||||
|
||||
const candidateSplashPaths = [
|
||||
join(__dirname, '../../public', 'splash.html'),
|
||||
];
|
||||
const foundSplash = candidateSplashPaths.find(p => existsSync(p));
|
||||
if (foundSplash) {
|
||||
splashWindow.loadFile(foundSplash);
|
||||
}
|
||||
|
||||
// 注释掉后端启动,便于快速调试前端
|
||||
// startSpringBoot();
|
||||
|
||||
// 快速调试模式 - 直接打开主窗口
|
||||
setTimeout(() => {
|
||||
openAppIfNotOpened();
|
||||
}, 1000);
|
||||
|
||||
// 注释掉超时机制
|
||||
/*
|
||||
setTimeout(() => {
|
||||
if (!appOpened) {
|
||||
console.error('后端启动超时,程序即将退出');
|
||||
app.quit();
|
||||
}
|
||||
}, 30000);
|
||||
*/
|
||||
|
||||
// 保守 CSP(仅允许自身脚本),避免引入不必要的外部脚本
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': ['script-src \'self\'']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
stopSpringBoot();
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
stopSpringBoot();
|
||||
});
|
||||
|
||||
ipcMain.on('message', (event, message) => {
|
||||
console.log(message);
|
||||
})
|
||||
5
electron-vue-template/src/main/preload.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {contextBridge, ipcRenderer} from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
sendMessage: (message: string) => ipcRenderer.send('message', message)
|
||||
})
|
||||
0
electron-vue-template/src/main/static/.gitkeep
Normal file
14
electron-vue-template/src/main/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "../../build/main",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
},
|
||||
"exclude": ["static"]
|
||||
}
|
||||
535
electron-vue-template/src/renderer/App.vue
Normal file
@@ -0,0 +1,535 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed, defineAsyncComponent } from 'vue'
|
||||
import { ElConfigProvider, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
// 图标已移至对应组件
|
||||
import 'element-plus/dist/index.css'
|
||||
import { authApi } from './api/auth'
|
||||
import { deviceApi, type DeviceItem, type DeviceQuota } from './api/device'
|
||||
import ZebraDashboard from './components/zebra/ZebraDashboard.vue'
|
||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
||||
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
||||
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
||||
const RakutenDashboard = defineAsyncComponent(() => import('./components/rakuten/RakutenDashboard.vue'))
|
||||
const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/AmazonDashboard.vue'))
|
||||
|
||||
// 导航历史栈
|
||||
const navigationHistory = ref<string[]>(['rakuten'])
|
||||
const currentHistoryIndex = ref(0)
|
||||
|
||||
// 应用状态
|
||||
const activeMenu = ref('rakuten')
|
||||
const isAuthenticated = ref(false)
|
||||
const showAuthDialog = ref(false)
|
||||
const showRegDialog = ref(false)
|
||||
const zhCnLocale = zhCn
|
||||
const currentUsername = ref('')
|
||||
const showDeviceDialog = ref(false)
|
||||
const deviceLoading = ref(false)
|
||||
const devices = ref<DeviceItem[]>([])
|
||||
const deviceQuota = ref<DeviceQuota>({ limit: 0, used: 0 })
|
||||
const userPermissions = ref<string>('')
|
||||
|
||||
// 菜单配置 - 复刻ERP客户端格式
|
||||
const menuConfig = [
|
||||
{ key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R' },
|
||||
{ key: 'amazon', name: 'Amazon', index: 'amazon', icon: 'A' },
|
||||
{ key: 'zebra', name: 'Zebra', index: 'zebra', icon: 'Z' },
|
||||
{ key: 'shopee', name: 'Shopee', index: 'shopee', icon: 'S' },
|
||||
]
|
||||
|
||||
// 权限检查 - 复刻ERP客户端逻辑
|
||||
function hasPermission(module: string) {
|
||||
// 默认显示的基础菜单(未登录时也显示)
|
||||
const defaultModules = ['rakuten', 'amazon', 'zebra']
|
||||
|
||||
if (!isAuthenticated.value) {
|
||||
return defaultModules.includes(module)
|
||||
}
|
||||
|
||||
const permissions = userPermissions.value
|
||||
if (!permissions) {
|
||||
return defaultModules.includes(module) // 没有权限信息时显示默认菜单
|
||||
}
|
||||
|
||||
// 简化权限检查:直接检查模块名是否在权限字符串中
|
||||
return permissions.includes(module)
|
||||
}
|
||||
|
||||
const visibleMenus = computed(() => menuConfig.filter(item => hasPermission(item.key)))
|
||||
|
||||
const canGoBack = computed(() => currentHistoryIndex.value > 0)
|
||||
const canGoForward = computed(() => currentHistoryIndex.value < navigationHistory.value.length - 1)
|
||||
|
||||
function showContent() {
|
||||
const loading = document.getElementById('loading')
|
||||
if (loading) {
|
||||
loading.style.opacity = '0'
|
||||
setTimeout(() => { loading.style.display = 'none' }, 100)
|
||||
}
|
||||
const app = document.getElementById('app-root')
|
||||
if (app) app.style.opacity = '1'
|
||||
}
|
||||
|
||||
function addToHistory(menu: string) {
|
||||
if (navigationHistory.value[currentHistoryIndex.value] !== menu) {
|
||||
navigationHistory.value = navigationHistory.value.slice(0, currentHistoryIndex.value + 1)
|
||||
navigationHistory.value.push(menu)
|
||||
currentHistoryIndex.value = navigationHistory.value.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (canGoBack.value) {
|
||||
currentHistoryIndex.value--
|
||||
activeMenu.value = navigationHistory.value[currentHistoryIndex.value]
|
||||
}
|
||||
}
|
||||
|
||||
function goForward() {
|
||||
if (canGoForward.value) {
|
||||
currentHistoryIndex.value++
|
||||
activeMenu.value = navigationHistory.value[currentHistoryIndex.value]
|
||||
}
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function handleMenuSelect(key: string) {
|
||||
// 检查是否需要认证
|
||||
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
|
||||
if (!isAuthenticated.value && authRequiredMenus.includes(key)) {
|
||||
showAuthDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
activeMenu.value = key
|
||||
addToHistory(key)
|
||||
}
|
||||
|
||||
async function handleLoginSuccess(data: { token: string; user: any }) {
|
||||
isAuthenticated.value = true
|
||||
showAuthDialog.value = false
|
||||
|
||||
try {
|
||||
currentUsername.value = data?.user?.username || currentUsername.value
|
||||
userPermissions.value = data?.permissions || data?.user?.permissions || ''
|
||||
} catch {}
|
||||
|
||||
// 登录成功后自动注册设备 - 简化版
|
||||
try {
|
||||
const username = data?.user?.username || currentUsername.value
|
||||
if (username) {
|
||||
await deviceApi.register({ username })
|
||||
}
|
||||
} catch (e) {
|
||||
// 设备注册失败不影响登录流程,静默处理
|
||||
console.warn('设备注册失败:', e)
|
||||
}
|
||||
}
|
||||
async function handleUserClick() {
|
||||
if (!isAuthenticated.value) {
|
||||
showAuthDialog.value = true
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确认退出登录?', '提示', { type: 'warning', confirmButtonText: '退出', cancelButtonText: '取消' })
|
||||
const token = localStorage.getItem('token') || ''
|
||||
try { await authApi.logout(token) } catch {}
|
||||
try { localStorage.removeItem('token') } catch {}
|
||||
isAuthenticated.value = false
|
||||
currentUsername.value = ''
|
||||
userPermissions.value = ''
|
||||
showAuthDialog.value = true
|
||||
showDeviceDialog.value = false
|
||||
ElMessage.success('已退出登录')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
function handleLoginCancel() {
|
||||
showAuthDialog.value = false
|
||||
}
|
||||
|
||||
function showRegisterDialog() {
|
||||
showAuthDialog.value = false
|
||||
showRegDialog.value = true
|
||||
}
|
||||
|
||||
function handleRegisterSuccess() {
|
||||
showRegDialog.value = false
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
|
||||
function backToLogin() {
|
||||
showRegDialog.value = false
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
|
||||
// 检查认证状态 - 复刻ERP客户端逻辑
|
||||
async function checkAuth() {
|
||||
const token = localStorage.getItem('token')
|
||||
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const response = await authApi.verifyToken(token)
|
||||
if (response.success) {
|
||||
isAuthenticated.value = true
|
||||
if (!currentUsername.value) {
|
||||
const u = getUsernameFromToken(token)
|
||||
if (u) currentUsername.value = u
|
||||
}
|
||||
userPermissions.value = response.permissions || ''
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要显示登录弹框
|
||||
if (!isAuthenticated.value && authRequiredMenus.includes(activeMenu.value)) {
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function getClientIdFromToken(token?: string) {
|
||||
try {
|
||||
const t = token || localStorage.getItem('token') || ''
|
||||
const payload = JSON.parse(atob(t.split('.')[1] || ''))
|
||||
return payload.clientId || ''
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
function getUsernameFromToken(token?: string) {
|
||||
try {
|
||||
const t = token || localStorage.getItem('token') || ''
|
||||
const payload = JSON.parse(atob(t.split('.')[1] || ''))
|
||||
return payload.username || ''
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
async function openDeviceManager() {
|
||||
if (!isAuthenticated.value) {
|
||||
showAuthDialog.value = true
|
||||
return
|
||||
}
|
||||
showDeviceDialog.value = true
|
||||
await fetchDeviceData()
|
||||
}
|
||||
|
||||
async function fetchDeviceData() {
|
||||
const username = (currentUsername.value || getUsernameFromToken()).trim()
|
||||
if (!username) {
|
||||
ElMessage.warning('未获取到用户名,请重新登录')
|
||||
return
|
||||
}
|
||||
try {
|
||||
deviceLoading.value = true
|
||||
const [quota, list] = await Promise.all([
|
||||
deviceApi.getQuota(username),
|
||||
deviceApi.list(username),
|
||||
])
|
||||
deviceQuota.value = quota || { limit: 0, used: 0 }
|
||||
const clientId = getClientIdFromToken()
|
||||
devices.value = (list || []).map(d => ({ ...d, isCurrent: d.deviceId === clientId })) as any
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '获取设备列表失败')
|
||||
} finally {
|
||||
deviceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', { confirmButtonText: '确定移除', cancelButtonText: '取消', type: 'warning' })
|
||||
await deviceApi.remove({ deviceId: row.deviceId })
|
||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
||||
if (row.isCurrent) {
|
||||
// 当前设备被移除,清理登录状态
|
||||
isAuthenticated.value = false
|
||||
showAuthDialog.value = true
|
||||
try { localStorage.removeItem('token') } catch {}
|
||||
}
|
||||
ElMessage.success('已移除设备')
|
||||
} catch (e) {
|
||||
/* 用户取消或失败 */
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
showContent()
|
||||
await checkAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="zhCnLocale">
|
||||
<div id="app-root" class="root">
|
||||
<div class="loading-container" id="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div class="erp-container">
|
||||
<div class="sidebar">
|
||||
<div class="user-avatar">
|
||||
<img src="/icon/icon.png" alt="logo" />
|
||||
</div>
|
||||
<div class="menu-group-title">电商平台</div>
|
||||
<ul class="menu">
|
||||
<li
|
||||
v-for="item in visibleMenus"
|
||||
:key="item.key"
|
||||
class="menu-item"
|
||||
:class="{ active: activeMenu === item.key }"
|
||||
@click="handleMenuSelect(item.key)"
|
||||
>
|
||||
<span class="menu-text"><span class="menu-icon" :data-k="item.key">{{ item.icon }}</span>{{ item.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<NavigationBar
|
||||
:can-go-back="canGoBack"
|
||||
:can-go-forward="canGoForward"
|
||||
:active-menu="activeMenu"
|
||||
@go-back="goBack"
|
||||
@go-forward="goForward"
|
||||
@reload="reloadPage"
|
||||
@user-click="handleUserClick"
|
||||
@open-device="openDeviceManager" />
|
||||
<div class="content-body">
|
||||
<div
|
||||
class="dashboard-home"
|
||||
v-if="!isAuthenticated && (activeMenu === 'rakuten' || activeMenu === 'amazon' || activeMenu === 'zebra')">
|
||||
<div class="icon-container">
|
||||
<img src="/image/111.png" alt="ERP Logo" class="main-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<ZebraDashboard v-if="activeMenu === 'zebra'" />
|
||||
<RakutenDashboard v-else-if="activeMenu === 'rakuten'" />
|
||||
<AmazonDashboard v-else-if="activeMenu === 'amazon'" />
|
||||
<div v-else class="placeholder">
|
||||
<div class="placeholder-card">
|
||||
<div class="placeholder-title">{{ activeMenu.toUpperCase() }} 面板</div>
|
||||
<div class="placeholder-desc">功能开发中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 认证组件 -->
|
||||
<LoginDialog
|
||||
v-model="showAuthDialog"
|
||||
@login-success="handleLoginSuccess"
|
||||
@show-register="showRegisterDialog" />
|
||||
|
||||
<RegisterDialog
|
||||
v-model="showRegDialog"
|
||||
@register-success="handleRegisterSuccess"
|
||||
@back-to-login="backToLogin" />
|
||||
|
||||
<!-- 设备管理弹框 -->
|
||||
<el-dialog
|
||||
:title="`设备管理 (${deviceQuota.used || 0}/${deviceQuota.limit || 0})`"
|
||||
v-model="showDeviceDialog"
|
||||
width="560px"
|
||||
:close-on-click-modal="false">
|
||||
<div style="margin-bottom: 10px; color:#909399;">当前账号可以授权绑定 {{ deviceQuota.limit }} 台设备</div>
|
||||
<el-table :data="devices" size="small" :loading="deviceLoading" style="width:100%" stripe>
|
||||
<el-table-column label="设备名" min-width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.name || scope.row.deviceId }}</span>
|
||||
<el-tag v-if="scope.row.isCurrent" size="small" type="success" style="margin-left:6px;">本机</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status==='online' ? 'success' : 'info'" size="small">{{ scope.row.status==='online' ? '已登录' : '已登出' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近" min-width="130">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.lastActiveAt || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="scope">
|
||||
<el-button type="text" size="small" style="color:#F56C6C" @click="confirmRemoveDevice(scope.row)">移除设备</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showDeviceDialog=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #f5f5f5;
|
||||
z-index: 9999;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #e6e6e6;
|
||||
border-top: 5px solid #409EFF;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.erp-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
min-width: 220px;
|
||||
flex-shrink: 0;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e8eaec;
|
||||
padding: 16px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.platform-icons { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
||||
.picon { width: 28px; height: 28px; object-fit: contain; }
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
.user-avatar img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: contain;
|
||||
background: #ffffff;
|
||||
}
|
||||
.menu-group-title {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin: 8px 6px 10px;
|
||||
text-align: left; /* “电商平台”四个字靠左 */
|
||||
}
|
||||
.menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #333333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.menu-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.menu-item.active {
|
||||
background: #ecf5ff !important;
|
||||
color: #409EFF !important;
|
||||
}
|
||||
.menu-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
.menu-text { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.menu-icon { display: inline-flex; width: 18px; height: 18px; border-radius: 4px; align-items: center; justify-content: center; font-size: 12px; color: #fff; }
|
||||
.menu-icon[data-k="rakuten"] { background: #BF0000; }
|
||||
.menu-icon[data-k="amazon"] { background: #FF9900; color: #1A1A1A; }
|
||||
.menu-icon[data-k="zebra"] { background: #34495e; }
|
||||
.menu-icon[data-k="shopee"] { background: #EE4D2D; }
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 导航栏和认证相关样式已移至对应组件 */
|
||||
|
||||
.content-body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dashboard-home {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
z-index: 100;
|
||||
}
|
||||
.icon-container { display: flex; justify-content: center; }
|
||||
.main-icon {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
}
|
||||
.placeholder-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 12px;
|
||||
padding: 24px 28px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
color: #2c3e50;
|
||||
}
|
||||
.placeholder-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
|
||||
.placeholder-desc { font-size: 13px; color: #606266; }
|
||||
</style>
|
||||
38
electron-vue-template/src/renderer/api/amazon.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { http } from './http';
|
||||
|
||||
export const amazonApi = {
|
||||
// 上传Excel文件解析ASIN列表
|
||||
importAsinFromExcel(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return http.upload<{ code: number, data: { asinList: string[], total: number }, msg: string | null }>('/api/amazon/import/asin', formData);
|
||||
},
|
||||
|
||||
getProductsBatch(asinList: string[], batchId: string) {
|
||||
return http.post<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/batch', { asinList, batchId });
|
||||
},
|
||||
getLatestProducts() {
|
||||
return http.get<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/latest');
|
||||
},
|
||||
getProductsByBatch(batchId: string) {
|
||||
return http.get<{ products: any[] }>(`/api/amazon/products/batch/${batchId}`);
|
||||
},
|
||||
updateProduct(productData: unknown) {
|
||||
return http.post('/api/amazon/products/update', productData);
|
||||
},
|
||||
deleteProduct(productId: string) {
|
||||
return http.post('/api/amazon/products/delete', { id: productId });
|
||||
},
|
||||
exportToExcel(products: unknown[], options: Record<string, unknown> = {}) {
|
||||
return http.post('/api/amazon/export', { products, ...options });
|
||||
},
|
||||
getProductStats() {
|
||||
return http.get('/api/amazon/stats');
|
||||
},
|
||||
searchProducts(searchParams: Record<string, unknown>) {
|
||||
return http.get('/api/amazon/products/search', searchParams);
|
||||
},
|
||||
openGenmaiSpirit() {
|
||||
return http.post('/api/genmai/open');
|
||||
},
|
||||
};
|
||||
90
electron-vue-template/src/renderer/api/auth.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { http } from './http';
|
||||
|
||||
// 统一响应处理函数 - 适配ERP客户端格式
|
||||
function unwrap<T>(res: any): T {
|
||||
if (res && typeof res.success === 'boolean') {
|
||||
if (!res.success) {
|
||||
const message: string = res.message || res.msg || '请求失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
// 兼容标准格式
|
||||
if (res && typeof res.code === 'number') {
|
||||
if (res.code !== 0) {
|
||||
const message: string = res.msg || '请求失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
return (res.data as T) ?? ({} as T);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
|
||||
// 认证相关类型定义
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
success: boolean;
|
||||
token: string;
|
||||
permissions: string[];
|
||||
username: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface RegisterResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface CheckUsernameResponse {
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
// 用户登录
|
||||
login(params: LoginRequest) {
|
||||
return http
|
||||
.post('/api/login', params)
|
||||
.then(res => unwrap<LoginResponse>(res));
|
||||
},
|
||||
|
||||
// 用户注册
|
||||
register(params: RegisterRequest) {
|
||||
return http
|
||||
.post('/api/register', params)
|
||||
.then(res => unwrap<RegisterResponse>(res));
|
||||
},
|
||||
|
||||
// 检查用户名可用性
|
||||
checkUsername(username: string) {
|
||||
return http
|
||||
.get('/api/check-username', { username })
|
||||
.then(res => {
|
||||
// checkUsername 使用标准格式 {code: 200, data: boolean}
|
||||
if (res && res.code === 200) {
|
||||
return { available: res.data };
|
||||
}
|
||||
throw new Error(res?.msg || '检查用户名失败');
|
||||
});
|
||||
},
|
||||
|
||||
// 验证token有效性
|
||||
verifyToken(token: string) {
|
||||
return http
|
||||
.post('/api/verify', { token })
|
||||
.then(res => unwrap<{ success: boolean }>(res));
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
logout(token: string) {
|
||||
return http.postVoid('/api/logout', { token });
|
||||
},
|
||||
};
|
||||
48
electron-vue-template/src/renderer/api/device.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { http } from './http'
|
||||
|
||||
// 与老版保持相同的接口路径与参数
|
||||
const base = '/api/device'
|
||||
|
||||
export interface DeviceQuota {
|
||||
limit: number
|
||||
used: number
|
||||
}
|
||||
|
||||
export interface DeviceItem {
|
||||
deviceId: string
|
||||
name?: string
|
||||
status?: 'online' | 'offline'
|
||||
lastActiveAt?: string
|
||||
}
|
||||
|
||||
export const deviceApi = {
|
||||
getQuota(username: string) {
|
||||
return http.get<DeviceQuota | any>(`${base}/quota`, { username }).then((res: any) => {
|
||||
if (res && typeof res.limit !== 'undefined') return res as DeviceQuota
|
||||
if (res && typeof res.code === 'number') return (res.data as DeviceQuota) || { limit: 0, used: 0 }
|
||||
return (res?.data as DeviceQuota) || { limit: 0, used: 0 }
|
||||
})
|
||||
},
|
||||
|
||||
list(username: string) {
|
||||
return http.get<DeviceItem[] | any>(`${base}/list`, { username }).then((res: any) => {
|
||||
if (Array.isArray(res)) return res as DeviceItem[]
|
||||
if (res && typeof res.code === 'number') return (res.data as DeviceItem[]) || []
|
||||
return (res?.data as DeviceItem[]) || []
|
||||
})
|
||||
},
|
||||
|
||||
register(payload: { username: string }) {
|
||||
return http.post(`${base}/register`, payload)
|
||||
},
|
||||
|
||||
remove(payload: { deviceId: string }) {
|
||||
return http.postVoid(`${base}/remove`, payload)
|
||||
},
|
||||
|
||||
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
|
||||
return http.postVoid(`${base}/heartbeat`, payload)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
78
electron-vue-template/src/renderer/api/http.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// 极简 HTTP 工具:仅封装 GET/POST,默认指向本地 8081
|
||||
export type HttpMethod = 'GET' | 'POST';
|
||||
|
||||
const BASE_URL = 'http://localhost:8081';
|
||||
|
||||
// 将对象转为查询字符串
|
||||
function buildQuery(params?: Record<string, unknown>): string {
|
||||
if (!params) return '';
|
||||
const usp = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null) return;
|
||||
usp.append(key, String(value));
|
||||
});
|
||||
const queryString = usp.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
}
|
||||
|
||||
// 统一请求入口:自动加上 BASE_URL、JSON 头与错误处理
|
||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(text || `HTTP ${res.status}`);
|
||||
}
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
return (await res.text()) as unknown as T;
|
||||
}
|
||||
|
||||
export const http = {
|
||||
get<T>(path: string, params?: Record<string, unknown>) {
|
||||
return request<T>(`${path}${buildQuery(params)}`, { method: 'GET' });
|
||||
},
|
||||
post<T>(path: string, body?: unknown) {
|
||||
return request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined });
|
||||
},
|
||||
// 用于无需读取响应体的 POST(如删除/心跳等),从根源避免读取中断
|
||||
postVoid(path: string, body?: unknown) {
|
||||
return fetch(`${BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).then(res => {
|
||||
if (!res.ok) return res.text().then(t => Promise.reject(new Error(t || `HTTP ${res.status}`)));
|
||||
return undefined as unknown as void;
|
||||
});
|
||||
},
|
||||
// 文件上传:透传 FormData,不设置 Content-Type 让浏览器自动处理
|
||||
upload<T>(path: string, form: FormData) {
|
||||
const res = fetch(`${BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
});
|
||||
return res.then(async response => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new Error(text || `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
38
electron-vue-template/src/renderer/api/rakuten.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { http } from './http';
|
||||
|
||||
function unwrap<T>(res: any): T {
|
||||
if (res && typeof res.code === 'number') {
|
||||
if (res.code !== 0) {
|
||||
const message: string = res.msg || '请求失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
return (res.data as T) ?? ({} as T);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
|
||||
export const rakutenApi = {
|
||||
// 上传 Excel 或按店铺名查询
|
||||
getProducts(params: { file?: File; shopName?: string; batchId?: string }) {
|
||||
const formData = new FormData();
|
||||
if (params.file) formData.append('file', params.file);
|
||||
if (params.batchId) formData.append('batchId', params.batchId);
|
||||
if (params.shopName) formData.append('shopName', params.shopName);
|
||||
return http
|
||||
.upload('/api/rakuten/products', formData)
|
||||
.then(res => unwrap<{ products: any[]; total?: number; sessionId?: string }>(res));
|
||||
},
|
||||
search1688(imageUrl: string, sessionId?: string) {
|
||||
const payload: Record<string, unknown> = { imageUrl };
|
||||
if (sessionId) payload.sessionId = sessionId;
|
||||
return http.post('/api/rakuten/search1688', payload).then(res => unwrap<any>(res));
|
||||
},
|
||||
getLatestProducts() {
|
||||
return http.get('/api/rakuten/products/latest').then(res => unwrap<{ products: any[] }>(res));
|
||||
},
|
||||
exportAndSave(exportData: unknown) {
|
||||
return http
|
||||
.post('/api/rakuten/export-and-save', exportData)
|
||||
.then(res => unwrap<{ filePath: string; fileName?: string; recordCount?: number; hasImages?: boolean }>(res));
|
||||
},
|
||||
};
|
||||
15
electron-vue-template/src/renderer/api/shopee.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { http } from './http';
|
||||
|
||||
export const shopeeApi = {
|
||||
getAdHosting(params: Record<string, unknown> = {}) {
|
||||
return http.get('/api/shopee/ad-hosting', params);
|
||||
},
|
||||
getReviews(params: Record<string, unknown> = {}) {
|
||||
return http.get('/api/shopee/reviews', params);
|
||||
},
|
||||
exportData(exportParams: Record<string, unknown> = {}) {
|
||||
return http.post('/api/shopee/export', exportParams);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
56
electron-vue-template/src/renderer/api/zebra.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// 斑马订单模型(根据页面所需字段精简定义)
|
||||
export interface ZebraOrder {
|
||||
orderedAt?: string;
|
||||
productImage?: string;
|
||||
productTitle?: string;
|
||||
shopOrderNumber?: string;
|
||||
timeSinceOrder?: string;
|
||||
priceJpy?: number;
|
||||
productQuantity?: number;
|
||||
shippingFeeJpy?: number;
|
||||
serviceFee?: string;
|
||||
productNumber?: string;
|
||||
poNumber?: string;
|
||||
shippingFeeCny?: number;
|
||||
internationalShippingFee?: number;
|
||||
poLogisticsCompany?: string;
|
||||
poTrackingNumber?: string;
|
||||
internationalTrackingNumber?: string;
|
||||
trackInfo?: string;
|
||||
}
|
||||
|
||||
export interface ZebraOrdersResp {
|
||||
orders: ZebraOrder[];
|
||||
total?: number;
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
import { http } from './http';
|
||||
|
||||
// 斑马 API:与原 zebra-api.js 对齐的接口封装
|
||||
export const zebraApi = {
|
||||
getOrders(params: Record<string, unknown>) {
|
||||
return http.get<ZebraOrdersResp>('/api/banma/orders', params);
|
||||
},
|
||||
getOrdersByBatch(batchId: string) {
|
||||
return http.get<ZebraOrdersResp>(`/api/banma/orders/batch/${batchId}`);
|
||||
},
|
||||
getLatestOrders() {
|
||||
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
|
||||
},
|
||||
getShops() {
|
||||
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>('/api/banma/shops');
|
||||
},
|
||||
refreshToken() {
|
||||
return http.post('/api/banma/refresh-token');
|
||||
},
|
||||
exportAndSaveOrders(exportData: unknown) {
|
||||
return http.post<{ filePath: string }>('/api/banma/export-and-save', exportData);
|
||||
},
|
||||
getOrderStats() {
|
||||
return http.get('/api/banma/orders/stats');
|
||||
},
|
||||
searchOrders(searchParams: Record<string, unknown>) {
|
||||
return http.get('/api/banma/orders/search', searchParams);
|
||||
},
|
||||
};
|
||||
1
electron-vue-template/src/renderer/assets/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
electron-vue-template/src/renderer/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1 @@
|
||||
<template></template>
|
||||
@@ -0,0 +1,393 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { amazonApi } from '../../api/amazon'
|
||||
|
||||
// 响应式状态
|
||||
const loading = ref(false) // 主加载状态
|
||||
const tableLoading = ref(false) // 表格加载状态
|
||||
const progressPercentage = ref(0) // 进度百分比
|
||||
const localProductData = ref<any[]>([]) // 本地产品数据
|
||||
const singleAsin = ref('') // 单个ASIN输入
|
||||
const currentAsin = ref('') // 当前处理的ASIN
|
||||
const genmaiLoading = ref(false) // Genmai Spirit加载状态
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((localProductData.value.length || 0) / pageSize.value)))
|
||||
const amazonUpload = ref<HTMLInputElement | null>(null)
|
||||
const dragActive = ref(false)
|
||||
|
||||
// 计算属性 - 当前页数据
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return localProductData.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 通用消息提示
|
||||
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
|
||||
alert(`[${type.toUpperCase()}] ${message}`)
|
||||
}
|
||||
|
||||
// Excel文件上传处理 - 主要业务逻辑入口
|
||||
async function processExcelFile(file: File) {
|
||||
try {
|
||||
loading.value = true
|
||||
tableLoading.value = true
|
||||
localProductData.value = []
|
||||
progressPercentage.value = 0
|
||||
|
||||
const response = await amazonApi.importAsinFromExcel(file)
|
||||
const asinList = response.data.asinList
|
||||
|
||||
if (!asinList || asinList.length === 0) {
|
||||
showMessage('文件中未找到有效的ASIN数据', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
showMessage(`成功解析 ${asinList.length} 个ASIN`, 'success')
|
||||
await batchGetProductInfo(asinList)
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '处理文件失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExcelUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
await processExcelFile(file)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true }
|
||||
function onDragLeave() { dragActive.value = false }
|
||||
async function onDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
dragActive.value = false
|
||||
const file = e.dataTransfer?.files?.[0]
|
||||
if (!file) return
|
||||
const ok = /(\.csv|\.txt|\.xls|\.xlsx)$/i.test(file.name)
|
||||
if (!ok) return showMessage('仅支持 .csv/.txt/.xls/.xlsx 文件', 'warning')
|
||||
await processExcelFile(file)
|
||||
}
|
||||
|
||||
// 批量获取产品信息 - 核心数据处理逻辑
|
||||
async function batchGetProductInfo(asinList: string[]) {
|
||||
try {
|
||||
currentAsin.value = '正在处理...'
|
||||
progressPercentage.value = 0
|
||||
localProductData.value = []
|
||||
|
||||
const batchId = `BATCH_${Date.now()}`
|
||||
const batchSize = 2 // 每批处理2个ASIN
|
||||
const totalBatches = Math.ceil(asinList.length / batchSize)
|
||||
let processedCount = 0
|
||||
let failedCount = 0
|
||||
|
||||
// 分批处理ASIN列表
|
||||
for (let i = 0; i < totalBatches && loading.value; i++) {
|
||||
const start = i * batchSize
|
||||
const end = Math.min(start + batchSize, asinList.length)
|
||||
const batchAsins = asinList.slice(start, end)
|
||||
|
||||
currentAsin.value = `正在处理第${i + 1}/${totalBatches}批 (${batchAsins.join(', ')})`
|
||||
|
||||
try {
|
||||
const result = await amazonApi.getProductsBatch(batchAsins, batchId)
|
||||
|
||||
if (result?.data?.products?.length > 0) {
|
||||
localProductData.value.push(...result.data.products)
|
||||
if (tableLoading.value) tableLoading.value = false // 首次数据到达后隐藏表格加载
|
||||
}
|
||||
|
||||
// 统计失败数量
|
||||
const expectedCount = batchAsins.length
|
||||
const actualCount = result?.data?.products?.length || 0
|
||||
failedCount += Math.max(0, expectedCount - actualCount)
|
||||
|
||||
} catch (error) {
|
||||
failedCount += batchAsins.length
|
||||
console.error(`批次${i + 1}失败:`, error)
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
processedCount += batchAsins.length
|
||||
progressPercentage.value = Math.round((processedCount / asinList.length) * 100)
|
||||
|
||||
// 批次间延迟,避免API频率限制
|
||||
if (i < totalBatches - 1 && loading.value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1500))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理完成状态更新
|
||||
progressPercentage.value = 100
|
||||
currentAsin.value = '处理完成'
|
||||
|
||||
// 结果提示
|
||||
if (failedCount > 0) {
|
||||
showMessage(`采集完成!共 ${asinList.length} 个ASIN,成功 ${asinList.length - failedCount} 个,失败 ${failedCount} 个`, 'warning')
|
||||
} else {
|
||||
showMessage(`采集完成!成功获取 ${asinList.length} 个产品信息`, 'success')
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '批量获取产品信息失败', 'error')
|
||||
currentAsin.value = '处理失败'
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 单个ASIN查询
|
||||
async function searchSingleAsin() {
|
||||
const asin = singleAsin.value.trim()
|
||||
if (!asin) return
|
||||
|
||||
localProductData.value = []
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const resp = await amazonApi.getProductsBatch([asin], `SINGLE_${Date.now()}`)
|
||||
if (resp?.data?.products?.length > 0) {
|
||||
localProductData.value = resp.data.products
|
||||
showMessage('查询成功', 'success')
|
||||
singleAsin.value = ''
|
||||
} else {
|
||||
showMessage('未找到商品信息', 'warning')
|
||||
}
|
||||
} catch (e: any) {
|
||||
showMessage(e?.message || '查询失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel数据
|
||||
async function exportToExcel() {
|
||||
if (!localProductData.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
showMessage('正在生成Excel文件,请稍候...', 'info')
|
||||
|
||||
// 数据格式化 - 只保留核心字段
|
||||
const exportData = localProductData.value.map(product => ({
|
||||
asin: product.asin || '',
|
||||
seller_shipper: getSellerShipperText(product),
|
||||
price: product.price || '无货'
|
||||
}))
|
||||
|
||||
await amazonApi.exportToExcel(exportData, {
|
||||
filename: `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
})
|
||||
|
||||
showMessage('Excel文件导出成功', 'success')
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '导出Excel失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取卖家/配送方信息 - 数据处理辅助函数
|
||||
function getSellerShipperText(product: any) {
|
||||
let text = product.seller || '无货'
|
||||
if (product.shipper && product.shipper !== product.seller) {
|
||||
text += (text && text !== '无货' ? ' / ' : '') + product.shipper
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// 停止获取操作
|
||||
function stopFetch() {
|
||||
loading.value = false
|
||||
currentAsin.value = '已停止'
|
||||
showMessage('已停止获取产品数据', 'info')
|
||||
}
|
||||
|
||||
// 打开Genmai Spirit工具
|
||||
async function openGenmaiSpirit() {
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await amazonApi.openGenmaiSpirit()
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '打开跟卖精灵失败', 'error')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleCurrentChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 使用 Element Plus 的 jumper,不再需要手动跳转函数
|
||||
|
||||
function openAmazonUpload() {
|
||||
amazonUpload.value?.click()
|
||||
}
|
||||
// 组件挂载时获取最新数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await amazonApi.getLatestProducts()
|
||||
localProductData.value = resp.data?.products || []
|
||||
} catch {
|
||||
// 静默处理初始化失败
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="amazon-root">
|
||||
<div class="main-container">
|
||||
|
||||
<!-- 文件导入和操作区域 -->
|
||||
<div class="import-section" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" :class="{ 'drag-active': dragActive }">
|
||||
<div class="import-controls">
|
||||
<!-- 文件上传按钮 -->
|
||||
<el-button type="primary" :disabled="loading" @click="openAmazonUpload">
|
||||
📂 {{ loading ? '处理中...' : '导入ASIN列表' }}
|
||||
</el-button>
|
||||
<input ref="amazonUpload" style="display:none" type="file" accept=".csv,.txt,.xls,.xlsx" @change="handleExcelUpload" :disabled="loading" />
|
||||
|
||||
<!-- 单个ASIN输入 -->
|
||||
<div class="single-input">
|
||||
<input class="text" v-model="singleAsin" placeholder="输入单个ASIN" :disabled="loading" @keyup.enter="searchSingleAsin" />
|
||||
<el-button type="info" :disabled="!singleAsin || loading" @click="searchSingleAsin">查询</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
<el-button type="success" :disabled="!localProductData.length || loading" @click="exportToExcel">导出Excel</el-button>
|
||||
<el-button type="warning" :loading="genmaiLoading" @click="openGenmaiSpirit">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条显示 -->
|
||||
<div class="progress-section" v-if="loading">
|
||||
<div class="progress-box">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-text">{{ progressPercentage }}%</div>
|
||||
</div>
|
||||
<div class="current-status" v-if="currentAsin">{{ currentAsin }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<!-- 数据表格(无数据时也显示表头) -->
|
||||
<div class="table-section">
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="130">ASIN</th>
|
||||
<th>卖家/配送方</th>
|
||||
<th width="120">当前售价</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="paginatedData.length === 0">
|
||||
<td colspan="3" class="empty-tip">暂无数据,请导入ASIN列表</td>
|
||||
</tr>
|
||||
<tr v-else v-for="row in paginatedData" :key="row.asin">
|
||||
<td>{{ row.asin }}</td>
|
||||
<td>
|
||||
<div class="seller-info">
|
||||
<span class="seller">{{ row.seller || '无货' }}</span>
|
||||
<span v-if="row.shipper && row.shipper !== row.seller" class="shipper">/ {{ row.shipper }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="price">{{ row.price || '无货' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 表格加载遮罩 -->
|
||||
<div v-if="tableLoading" class="table-loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-fixed" >
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="localProductData.length"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
|
||||
.import-section { margin-bottom: 10px; flex-shrink: 0; }
|
||||
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.single-input { display: flex; align-items: center; gap: 8px; }
|
||||
.text { width: 180px; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; outline: none; transition: border-color 0.2s ease; }
|
||||
.text:focus { border-color: #409EFF; }
|
||||
.text:disabled { background: #f5f7fa; color: #c0c4cc; }
|
||||
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.progress-section { margin: 15px 0 10px 0; }
|
||||
.progress-box { padding: 8px 0; }
|
||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
|
||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
.table-wrapper { height: 100%; overflow: auto; }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
|
||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
||||
.table tbody tr:hover { background: #f9f9f9; }
|
||||
.seller-info { display: flex; align-items: center; gap: 4px; }
|
||||
.seller { color: #303133; font-weight: 500; }
|
||||
.shipper { color: #909399; font-size: 12px; }
|
||||
.price { color: #e6a23c; font-weight: 600; }
|
||||
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
|
||||
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
|
||||
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
|
||||
.import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AmazonDashboard',
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { authApi } from '../../api/auth'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'loginSuccess', data: { token: string; user: any }): void
|
||||
(e: 'showRegister'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const authForm = ref({ username: '', password: '' })
|
||||
const authLoading = ref(false)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
async function handleAuth() {
|
||||
if (!authForm.value.username || !authForm.value.password) return
|
||||
|
||||
authLoading.value = true
|
||||
try {
|
||||
const data = await authApi.login(authForm.value)
|
||||
localStorage.setItem('token', data.token)
|
||||
emit('loginSuccess', {
|
||||
token: data.token,
|
||||
user: {
|
||||
username: data.username,
|
||||
permissions: data.permissions
|
||||
}
|
||||
})
|
||||
ElMessage.success('登录成功')
|
||||
resetForm()
|
||||
} catch (err) {
|
||||
ElMessage.error((err as Error).message)
|
||||
} finally {
|
||||
authLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAuth() {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
authForm.value = { username: '', password: '' }
|
||||
}
|
||||
|
||||
function showRegister() {
|
||||
emit('showRegister')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
title="用户登录"
|
||||
v-model="visible"
|
||||
:close-on-click-modal="false"
|
||||
width="400px"
|
||||
center>
|
||||
<div style="text-align: center; padding: 20px 0;">
|
||||
<div style="margin-bottom: 30px; color: #666;">
|
||||
<el-icon size="48" color="#409EFF"><User /></el-icon>
|
||||
<p style="margin-top: 15px; font-size: 16px;">请登录以使用系统功能</p>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="authForm.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
style="margin-bottom: 15px;"
|
||||
:disabled="authLoading"
|
||||
@keyup.enter="handleAuth">
|
||||
</el-input>
|
||||
|
||||
<el-input
|
||||
v-model="authForm.password"
|
||||
placeholder="请输入密码"
|
||||
type="password"
|
||||
size="large"
|
||||
style="margin-bottom: 20px;"
|
||||
:disabled="authLoading"
|
||||
@keyup.enter="handleAuth">
|
||||
</el-input>
|
||||
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="authLoading"
|
||||
:disabled="!authForm.username || !authForm.password || authLoading"
|
||||
@click="handleAuth"
|
||||
style="width: 120px; margin-right: 10px;">
|
||||
登录
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
:disabled="authLoading"
|
||||
@click="cancelAuth"
|
||||
style="width: 120px;">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<el-button type="text" @click="showRegister" :disabled="authLoading">
|
||||
还没有账号?点击注册
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { authApi } from '../../api/auth'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'registerSuccess'): void
|
||||
(e: 'backToLogin'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const registerForm = ref({ username: '', password: '', confirmPassword: '' })
|
||||
const registerLoading = ref(false)
|
||||
const usernameCheckResult = ref<boolean | null>(null)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const canRegister = computed(() => {
|
||||
const { username, password, confirmPassword } = registerForm.value
|
||||
return username &&
|
||||
password.length >= 6 &&
|
||||
password === confirmPassword &&
|
||||
usernameCheckResult.value === true
|
||||
})
|
||||
|
||||
async function checkUsernameAvailability() {
|
||||
if (!registerForm.value.username) {
|
||||
usernameCheckResult.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await authApi.checkUsername(registerForm.value.username)
|
||||
usernameCheckResult.value = data.available
|
||||
} catch {
|
||||
usernameCheckResult.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
if (!canRegister.value) return
|
||||
|
||||
registerLoading.value = true
|
||||
try {
|
||||
const result = await authApi.register({
|
||||
username: registerForm.value.username,
|
||||
password: registerForm.value.password
|
||||
})
|
||||
ElMessage.success(result.message || '注册成功,请登录')
|
||||
emit('registerSuccess')
|
||||
resetForm()
|
||||
} catch (err) {
|
||||
ElMessage.error((err as Error).message)
|
||||
} finally {
|
||||
registerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRegister() {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
registerForm.value = { username: '', password: '', confirmPassword: '' }
|
||||
usernameCheckResult.value = null
|
||||
}
|
||||
|
||||
function backToLogin() {
|
||||
emit('backToLogin')
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
title="账号注册"
|
||||
v-model="visible"
|
||||
:close-on-click-modal="false"
|
||||
width="450px"
|
||||
center>
|
||||
<div style="text-align: center; padding: 20px 0;">
|
||||
<div style="margin-bottom: 20px; color: #666;">
|
||||
<el-icon size="48" color="#67C23A"><User /></el-icon>
|
||||
<p style="margin-top: 15px; font-size: 16px;">创建新账号</p>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="registerForm.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
style="margin-bottom: 15px;"
|
||||
:disabled="registerLoading"
|
||||
@blur="checkUsernameAvailability">
|
||||
</el-input>
|
||||
|
||||
<div v-if="usernameCheckResult !== null" style="margin-bottom: 15px; text-align: left;">
|
||||
<span v-if="usernameCheckResult" style="color: #67C23A; font-size: 12px;">
|
||||
✓ 用户名可用
|
||||
</span>
|
||||
<span v-else style="color: #F56C6C; font-size: 12px;">
|
||||
✗ 用户名已存在
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
placeholder="请输入密码(至少6位)"
|
||||
type="password"
|
||||
size="large"
|
||||
style="margin-bottom: 15px;"
|
||||
:disabled="registerLoading">
|
||||
</el-input>
|
||||
|
||||
<el-input
|
||||
v-model="registerForm.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
size="large"
|
||||
style="margin-bottom: 20px;"
|
||||
:disabled="registerLoading">
|
||||
</el-input>
|
||||
|
||||
<div>
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
:loading="registerLoading"
|
||||
:disabled="!canRegister || registerLoading"
|
||||
@click="handleRegister"
|
||||
style="width: 120px; margin-right: 10px;">
|
||||
注册
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
:disabled="registerLoading"
|
||||
@click="cancelRegister"
|
||||
style="width: 120px;">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<el-button type="text" @click="backToLogin" :disabled="registerLoading">
|
||||
已有账号?返回登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, ArrowRight, Refresh, Monitor, Setting, User } from '@element-plus/icons-vue'
|
||||
|
||||
interface Props {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
activeMenu: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'go-back'): void
|
||||
(e: 'go-forward'): void
|
||||
(e: 'reload'): void
|
||||
(e: 'user-click'): void
|
||||
(e: 'open-device'): void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="top-navbar">
|
||||
<div class="navbar-left">
|
||||
<div class="nav-controls">
|
||||
<button class="nav-btn" title="后退" @click="$emit('go-back')" :disabled="!canGoBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
</button>
|
||||
<button class="nav-btn" title="前进" @click="$emit('go-forward')" :disabled="!canGoForward">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-center">
|
||||
<div class="breadcrumbs">
|
||||
<span>首页</span>
|
||||
<span class="separator">></span>
|
||||
<span>{{ activeMenu }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<button class="nav-btn-round" title="刷新" @click="$emit('reload')">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</button>
|
||||
<button class="nav-btn-round" title="设备管理" @click="$emit('open-device')">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</button>
|
||||
<button class="nav-btn-round" title="设置">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</button>
|
||||
<button class="nav-btn-round" title="用户" @click="$emit('user-click')">
|
||||
<el-icon><User /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.top-navbar {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.navbar-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.nav-controls {
|
||||
display: flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-btn:hover:not(:disabled) {
|
||||
background: #f5f7fa;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.nav-btn:focus,
|
||||
.nav-btn:active {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
background: #f5f5f5;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.nav-btn:not(:last-child) {
|
||||
border-right: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.nav-btn-round {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-btn-round:hover {
|
||||
background: #f5f7fa;
|
||||
color: #409EFF;
|
||||
border-color: #c6e2ff;
|
||||
}
|
||||
|
||||
.nav-btn-round:focus,
|
||||
.nav-btn-round:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 8px;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,632 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted} from 'vue'
|
||||
import {rakutenApi} from '../../api/rakuten'
|
||||
|
||||
// UI 与加载状态
|
||||
const loading = ref(false)
|
||||
const tableLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusType = ref<'info' | 'success' | 'warning' | 'error'>('info')
|
||||
|
||||
// 查询与上传
|
||||
const singleShopName = ref('')
|
||||
const currentBatchId = ref('')
|
||||
const uploadInputRef = ref<HTMLInputElement | null>(null)
|
||||
const dragActive = ref(false)
|
||||
|
||||
// 数据与分页
|
||||
const allProducts = ref<any[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return allProducts.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 进度(完成后仍保持显示)
|
||||
const progressStarted = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
const totalProducts = ref(0)
|
||||
const processedProducts = ref(0)
|
||||
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleCurrentChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function openRakutenUpload() {
|
||||
uploadInputRef.value?.click()
|
||||
}
|
||||
|
||||
function parseSkuPrices(product: any) {
|
||||
if (!product.skuPrice) return []
|
||||
try {
|
||||
let skuStr = product.skuPrice
|
||||
if (typeof skuStr === 'string') {
|
||||
skuStr = skuStr.replace(/(\d+(?:\.\d+)?):"/g, '"$1":"')
|
||||
skuStr = JSON.parse(skuStr)
|
||||
}
|
||||
return Object.keys(skuStr).map(p => parseFloat(p)).filter(n => !isNaN(n)).sort((a, b) => a - b)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLatest() {
|
||||
const resp = await rakutenApi.getLatestProducts()
|
||||
allProducts.value = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
|
||||
}
|
||||
|
||||
async function searchProductInternal(product: any) {
|
||||
if (!product || !product.imgUrl) return
|
||||
if (product.mapRecognitionLink && String(product.mapRecognitionLink).trim() !== '') return
|
||||
const res = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
|
||||
const data = res
|
||||
Object.assign(product, {
|
||||
mapRecognitionLink: data.mapRecognitionLink,
|
||||
freight: data.freight,
|
||||
median: data.median,
|
||||
weight: data.weight,
|
||||
skuPrice: data.skuPrice,
|
||||
skuPrices: parseSkuPrices(data),
|
||||
image1688Url: data.mapRecognitionLink,
|
||||
detailUrl1688: data.mapRecognitionLink,
|
||||
})
|
||||
}
|
||||
|
||||
function beforeUpload(file: File) {
|
||||
const ok = /\.xlsx?$/.test(file.name)
|
||||
if (!ok) alert('仅支持 .xlsx/.xls 文件')
|
||||
return ok
|
||||
}
|
||||
|
||||
async function processFile(file: File) {
|
||||
if (!beforeUpload(file)) return
|
||||
progressStarted.value = true
|
||||
progressPercentage.value = 0
|
||||
totalProducts.value = 0
|
||||
processedProducts.value = 0
|
||||
loading.value = true
|
||||
tableLoading.value = true
|
||||
currentBatchId.value = `RAKUTEN_${Date.now()}`
|
||||
try {
|
||||
const resp = await rakutenApi.getProducts({file, batchId: currentBatchId.value})
|
||||
const products = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
allProducts.value = products
|
||||
statusMessage.value = `已获取 ${allProducts.value.length} 个乐天商品`
|
||||
|
||||
const needSearch = allProducts.value.filter(p => p && p.imgUrl && !p.mapRecognitionLink)
|
||||
if (needSearch.length > 0) {
|
||||
statusType.value = 'info'
|
||||
statusMessage.value = `已获取 ${allProducts.value.length} 个乐天商品,正在自动获取1688数据...`
|
||||
await startBatch1688Search(needSearch)
|
||||
} else {
|
||||
statusType.value = 'success'
|
||||
statusMessage.value = `已获取 ${allProducts.value.length} 个乐天商品,所有数据已完整!`
|
||||
}
|
||||
} catch (e: any) {
|
||||
statusMessage.value = e?.message || '上传失败'
|
||||
statusType.value = 'error'
|
||||
} finally {
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExcelUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files && input.files[0]
|
||||
if (!file) return
|
||||
await processFile(file)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true }
|
||||
function onDragLeave() { dragActive.value = false }
|
||||
async function onDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
dragActive.value = false
|
||||
const file = e.dataTransfer?.files?.[0]
|
||||
if (!file) return
|
||||
await processFile(file)
|
||||
}
|
||||
|
||||
async function searchSingleShop() {
|
||||
const shop = singleShopName.value.trim()
|
||||
if (!shop) return
|
||||
|
||||
// 重置进度与状态
|
||||
progressStarted.value = true
|
||||
progressPercentage.value = 0
|
||||
totalProducts.value = 0
|
||||
processedProducts.value = 0
|
||||
loading.value = true
|
||||
tableLoading.value = true
|
||||
currentBatchId.value = `RAKUTEN_${Date.now()}`
|
||||
try {
|
||||
const resp = await rakutenApi.getProducts({shopName: shop, batchId: currentBatchId.value})
|
||||
allProducts.value = (resp.products || []).filter((p: any) => p.originalShopName === shop).map(p => ({ ...p, skuPrices: parseSkuPrices(p) }))
|
||||
statusMessage.value = `店铺 ${shop} 共 ${allProducts.value.length} 条`
|
||||
singleShopName.value = ''
|
||||
|
||||
const needSearch = allProducts.value.filter(p => p && p.imgUrl && !p.mapRecognitionLink)
|
||||
if (needSearch.length > 0) {
|
||||
await startBatch1688Search(needSearch)
|
||||
} else if (allProducts.value.length > 0) {
|
||||
statusType.value = 'success'
|
||||
statusMessage.value = `店铺 ${shop} 的数据已加载完成,所有1688链接都已存在!`
|
||||
progressPercentage.value = 100
|
||||
}
|
||||
} catch (e: any) {
|
||||
statusMessage.value = e?.message || '查询失败'
|
||||
statusType.value = 'error'
|
||||
} finally {
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function stopTask() {
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
statusType.value = 'warning'
|
||||
statusMessage.value = '任务已停止'
|
||||
// 保留进度条和当前进度
|
||||
allProducts.value = allProducts.value.map(p => ({...p, searching1688: false}))
|
||||
}
|
||||
|
||||
async function startBatch1688Search(products: any[]) {
|
||||
const items = (products || []).filter(p => p && p.imgUrl && !p.mapRecognitionLink)
|
||||
if (items.length === 0) {
|
||||
progressPercentage.value = 100
|
||||
statusType.value = 'success'
|
||||
statusMessage.value = '所有商品都已获取1688数据!'
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
totalProducts.value = items.length
|
||||
processedProducts.value = 0
|
||||
progressStarted.value = true
|
||||
progressPercentage.value = 0
|
||||
statusType.value = 'info'
|
||||
statusMessage.value = `正在获取1688数据,共 ${totalProducts.value} 个商品...`
|
||||
await serialSearch1688(items)
|
||||
|
||||
if (processedProducts.value >= totalProducts.value) {
|
||||
progressPercentage.value = 100
|
||||
statusType.value = 'success'
|
||||
const successCount = allProducts.value.filter(p => p && p.mapRecognitionLink && String(p.mapRecognitionLink).trim() !== '').length
|
||||
statusMessage.value = `成功获取 ${successCount} 个`
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function serialSearch1688(products: any[]) {
|
||||
for (let i = 0; i < products.length && loading.value; i++) {
|
||||
const product = products[i]
|
||||
product.searching1688 = true
|
||||
await nextTickSafe()
|
||||
await searchProductInternal(product)
|
||||
product.searching1688 = false
|
||||
processedProducts.value++
|
||||
progressPercentage.value = Math.floor((processedProducts.value / Math.max(1, totalProducts.value)) * 100)
|
||||
if (i < products.length - 1 && loading.value) {
|
||||
await delay(500 + Math.random() * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function nextTickSafe() {
|
||||
// 不额外引入 nextTick,使用微任务刷新即可,保持体积精简
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
try {
|
||||
if (allProducts.value.length === 0) return alert('没有数据可导出')
|
||||
exportLoading.value = true
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
||||
const fileName = `乐天商品数据_${timestamp}.xlsx`
|
||||
const payload = {
|
||||
products: allProducts.value,
|
||||
title: '乐天商品数据导出',
|
||||
fileName,
|
||||
timestamp: new Date().toLocaleString('zh-CN'),
|
||||
// 传给后端的可选提示参数
|
||||
useMultiThread: true,
|
||||
chunkSize: 300,
|
||||
skipImages: allProducts.value.length > 200,
|
||||
}
|
||||
const resp = await rakutenApi.exportAndSave(payload)
|
||||
alert(`Excel文件已保存到: ${resp.filePath}`)
|
||||
} catch (e: any) {
|
||||
alert(e?.message || '导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(loadLatest)
|
||||
</script>
|
||||
<template>
|
||||
<div class="rakuten-root">
|
||||
<div class="main-container">
|
||||
|
||||
<!-- 文件导入和操作区域 -->
|
||||
<div class="import-section" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" :class="{ 'drag-active': dragActive }">
|
||||
<div class="import-controls">
|
||||
<!-- 文件上传按钮 -->
|
||||
<el-button type="primary" :disabled="loading" @click="openRakutenUpload">
|
||||
📂 {{ loading ? '处理中...' : '导入店铺名列表' }}
|
||||
</el-button>
|
||||
<input ref="uploadInputRef" style="display:none" type="file" accept=".xlsx,.xls" @change="handleExcelUpload"
|
||||
:disabled="loading"/>
|
||||
|
||||
<!-- 单个店铺名输入 -->
|
||||
<div class="single-input">
|
||||
<el-input v-model="singleShopName" placeholder="输入单个店铺名" :disabled="loading"
|
||||
@keyup.enter="searchSingleShop" style="width: 140px"/>
|
||||
<el-button type="info" :disabled="!singleShopName || loading" @click="searchSingleShop">查询</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="danger" :disabled="!loading" @click="stopTask">停止获取</el-button>
|
||||
<el-button type="success" :disabled="!allProducts.length || loading" @click="exportToExcel">导出Excel
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条显示 -->
|
||||
<div class="progress-section" v-if="progressStarted">
|
||||
<div class="progress-box">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-text">{{ progressPercentage }}%</div>
|
||||
</div>
|
||||
<div class="current-status" v-if="statusMessage">{{ statusMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<!-- 数据表格(无数据时也显示表头) -->
|
||||
<div class="table-section">
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店铺名</th>
|
||||
<th>商品链接</th>
|
||||
<th>商品图片</th>
|
||||
<th>排名</th>
|
||||
<th>商品标题</th>
|
||||
<th>价格</th>
|
||||
<th>1688识图链接</th>
|
||||
<th>1688运费</th>
|
||||
<th>1688中位价</th>
|
||||
<th>1688最低价</th>
|
||||
<th>1688中间价</th>
|
||||
<th>1688最高价</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="paginatedData.length === 0">
|
||||
<td colspan="12" class="empty-tip">暂无数据,请导入店铺名列表</td>
|
||||
</tr>
|
||||
<tr v-else v-for="row in paginatedData" :key="row.productUrl + (row.productTitle || '')">
|
||||
<td class="truncate shop-col" :title="row.originalShopName">{{ row.originalShopName }}</td>
|
||||
<td class="truncate url-col">
|
||||
<el-input v-if="row.productUrl" :value="row.productUrl" readonly @click="$event.target.select()" size="small"/>
|
||||
<span v-else>--</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="image-container" v-if="row.imgUrl">
|
||||
<img :src="row.imgUrl" class="thumb" alt="thumb"/>
|
||||
</div>
|
||||
<span v-else>无图片</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="row.ranking">{{ row.ranking }}</span>
|
||||
<span v-else>--</span>
|
||||
</td>
|
||||
<td class="truncate" :title="row.productTitle">{{ row.productTitle || '--' }}</td>
|
||||
<td>{{ row.price ? row.price + '円' : '--' }}</td>
|
||||
<td class="truncate url-col">
|
||||
<el-input v-if="row.mapRecognitionLink" :value="row.mapRecognitionLink" readonly @click="$event.target.select()" size="small"/>
|
||||
<span v-else-if="row.searching1688">搜索中...</span>
|
||||
<span v-else>--</span>
|
||||
</td>
|
||||
<td>{{ row.freight ?? '--' }}</td>
|
||||
<td>{{ row.median ?? '--' }}</td>
|
||||
<td>{{ row.skuPrices?.[0] ?? '--' }}</td>
|
||||
<td>{{ row.skuPrices?.[Math.floor(row.skuPrices.length / 2)] ?? '--' }}</td>
|
||||
<td>{{ row.skuPrices?.[row.skuPrices.length - 1] ?? '--' }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 表格加载遮罩 -->
|
||||
<div v-if="tableLoading" class="table-loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-fixed" >
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="allProducts.length"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rakuten-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.import-section {
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.import-controls {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.single-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
.progress-box {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-right: 50px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #ebeef5;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #409EFF, #66b1ff);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
font-size: 13px;
|
||||
color: #409EFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.current-status {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-weight: 600;
|
||||
padding: 8px 6px;
|
||||
border-bottom: 2px solid #ebeef5;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
max-width: 260px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.shop-col { max-width: 160px; }
|
||||
.url-col { max-width: 220px; }
|
||||
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
|
||||
.import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.table-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
font-size: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-fixed {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #ebeef5;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RakutenDashboard',
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { zebraApi, type ZebraOrder } from '../../api/zebra'
|
||||
|
||||
type Shop = { id: string; shopName: string }
|
||||
|
||||
const shopList = ref<Shop[]>([])
|
||||
const selectedShops = ref<string[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
const showProgress = ref(false)
|
||||
const allOrderData = ref<ZebraOrder[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
|
||||
// 批量获取状态
|
||||
const fetchCurrentPage = ref(1)
|
||||
const fetchTotalPages = ref(0)
|
||||
const fetchTotalItems = ref(0)
|
||||
const isFetching = ref(false)
|
||||
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return allOrderData.value.slice(start, end)
|
||||
})
|
||||
|
||||
function formatJpy(v?: number) {
|
||||
const n = Number(v || 0)
|
||||
return `¥${n.toLocaleString('ja-JP')}`
|
||||
}
|
||||
|
||||
function formatCny(v?: number) {
|
||||
const n = Number(v || 0)
|
||||
return `¥${n.toLocaleString('zh-CN')}`
|
||||
}
|
||||
|
||||
async function loadShops() {
|
||||
try {
|
||||
const resp = await zebraApi.getShops()
|
||||
const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? []
|
||||
shopList.value = list
|
||||
} catch (e) {
|
||||
console.error('获取店铺列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleCurrentChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (isFetching.value) return
|
||||
|
||||
loading.value = true
|
||||
isFetching.value = true
|
||||
showProgress.value = true
|
||||
progressPercentage.value = 0
|
||||
allOrderData.value = []
|
||||
fetchCurrentPage.value = 1
|
||||
fetchTotalItems.value = 0
|
||||
|
||||
const [startDate = '', endDate = ''] = dateRange.value || []
|
||||
await fetchPageData(startDate, endDate)
|
||||
}
|
||||
|
||||
async function fetchPageData(startDate: string, endDate: string) {
|
||||
if (!isFetching.value) return
|
||||
|
||||
try {
|
||||
const data = await zebraApi.getOrders({
|
||||
startDate,
|
||||
endDate,
|
||||
page: fetchCurrentPage.value,
|
||||
pageSize: 50,
|
||||
shopIds: selectedShops.value.join(',')
|
||||
})
|
||||
|
||||
const orders = data.orders || []
|
||||
allOrderData.value = [...allOrderData.value, ...orders]
|
||||
|
||||
fetchTotalPages.value = data.totalPages || 0
|
||||
fetchTotalItems.value = data.total || 0
|
||||
|
||||
if (fetchCurrentPage.value < fetchTotalPages.value && isFetching.value) {
|
||||
progressPercentage.value = Math.round((fetchCurrentPage.value / fetchTotalPages.value) * 100)
|
||||
fetchCurrentPage.value++
|
||||
setTimeout(() => fetchPageData(startDate, endDate), 200)
|
||||
} else {
|
||||
progressPercentage.value = 100
|
||||
finishFetching()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单数据失败:', e)
|
||||
finishFetching()
|
||||
}
|
||||
}
|
||||
|
||||
function finishFetching() {
|
||||
isFetching.value = false
|
||||
loading.value = false
|
||||
// 确保进度条完全填满
|
||||
progressPercentage.value = 100
|
||||
currentPage.value = 1
|
||||
// 进度条保留显示,不自动隐藏
|
||||
}
|
||||
|
||||
function stopFetch() {
|
||||
isFetching.value = false
|
||||
loading.value = false
|
||||
// 进度条保留显示,不自动隐藏
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
if (!allOrderData.value.length) return
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value })
|
||||
alert(`Excel文件已保存到: ${result.filePath}`)
|
||||
} catch (e) {
|
||||
alert('导出Excel失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadShops()
|
||||
try {
|
||||
const latest = await zebraApi.getLatestOrders()
|
||||
allOrderData.value = latest?.orders || []
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="zebra-root">
|
||||
<div class="main-container">
|
||||
|
||||
<!-- 筛选和操作区域 -->
|
||||
<div class="import-section">
|
||||
<div class="import-controls">
|
||||
<!-- 店铺选择 -->
|
||||
<el-select v-model="selectedShops" multiple placeholder="选择店铺" style="width: 260px;" :disabled="loading">
|
||||
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 200px;"
|
||||
:disabled="loading"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" :disabled="loading" @click="fetchData">
|
||||
📂 {{ loading ? '处理中...' : '获取订单数据' }}
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
<el-button type="success" :disabled="exportLoading || !allOrderData.length" @click="exportToExcel">导出Excel</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条显示 -->
|
||||
<div class="progress-section" v-if="showProgress">
|
||||
<div class="progress-box">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-text">{{ progressPercentage }}%</div>
|
||||
</div>
|
||||
<div class="current-status" v-if="fetchTotalItems > 0">
|
||||
{{ progressPercentage >= 100 ? '完成' : `获取中... (${allOrderData.length}/${fetchTotalItems})` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<!-- 数据表格(无数据时也显示表头) -->
|
||||
<div class="table-section">
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>下单时间</th>
|
||||
<th>商品图片</th>
|
||||
<th>商品名称</th>
|
||||
<th>乐天订单号</th>
|
||||
<th>下单距今</th>
|
||||
<th>订单金额/日元</th>
|
||||
<th>数量</th>
|
||||
<th>税费/日元</th>
|
||||
<th>回款抽点rmb</th>
|
||||
<th>商品番号</th>
|
||||
<th>1688订单号</th>
|
||||
<th>采购金额/rmb</th>
|
||||
<th>国际运费/rmb</th>
|
||||
<th>国内物流</th>
|
||||
<th>国内单号</th>
|
||||
<th>日本单号</th>
|
||||
<th>地址状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="paginatedData.length === 0">
|
||||
<td colspan="16" class="empty-tip">暂无数据,请选择日期范围获取订单</td>
|
||||
</tr>
|
||||
<tr v-else v-for="row in paginatedData" :key="row.shopOrderNumber + (row.productNumber || '')">
|
||||
<td>{{ row.orderedAt || '-' }}</td>
|
||||
<td>
|
||||
<div class="image-container" v-if="row.productImage">
|
||||
<img :src="row.productImage" class="thumb" alt="thumb" />
|
||||
</div>
|
||||
<span v-else>无图片</span>
|
||||
</td>
|
||||
<td class="truncate" :title="row.productTitle">{{ row.productTitle }}</td>
|
||||
<td class="truncate" :title="row.shopOrderNumber">{{ row.shopOrderNumber }}</td>
|
||||
<td>{{ row.timeSinceOrder || '-' }}</td>
|
||||
<td><span class="price-tag">{{ formatJpy(row.priceJpy) }}</span></td>
|
||||
<td>{{ row.productQuantity || 0 }}</td>
|
||||
<td><span class="fee-tag">{{ formatJpy(row.shippingFeeJpy) }}</span></td>
|
||||
<td>{{ row.serviceFee || '-' }}</td>
|
||||
<td class="truncate" :title="row.productNumber">{{ row.productNumber }}</td>
|
||||
<td class="truncate" :title="row.poNumber">{{ row.poNumber }}</td>
|
||||
<td><span class="fee-tag">{{ formatCny(row.shippingFeeCny) }}</span></td>
|
||||
<td>{{ row.internationalShippingFee || '-' }}</td>
|
||||
<td>{{ row.poLogisticsCompany || '-' }}</td>
|
||||
<td class="truncate" :title="row.poTrackingNumber">{{ row.poTrackingNumber }}</td>
|
||||
<td class="truncate" :title="row.internationalTrackingNumber">{{ row.internationalTrackingNumber }}</td>
|
||||
<td>
|
||||
<span v-if="row.trackInfo" class="tag">{{ row.trackInfo }}</span>
|
||||
<span v-else>暂无</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 表格加载遮罩 -->
|
||||
<div v-if="loading && !allOrderData.length" class="table-loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-fixed">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="allOrderData.length"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ZebraDashboard',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
|
||||
.import-section { margin-bottom: 10px; flex-shrink: 0; }
|
||||
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.progress-section { margin: 15px 0 10px 0; }
|
||||
.progress-box { padding: 8px 0; }
|
||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
|
||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
||||
.empty-section { flex: 1; display: flex; justify-content: center; align-items: center; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; }
|
||||
.empty-container { text-align: center; }
|
||||
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.6; }
|
||||
.empty-text { font-size: 14px; color: #909399; }
|
||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
.table-wrapper { height: 100%; overflow: auto; }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
|
||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
||||
.table tbody tr:hover { background: #f9f9f9; }
|
||||
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.image-container { display: flex; justify-content: center; align-items: center; width: 24px; height: 20px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
|
||||
.thumb { width: 16px; height: 16px; object-fit: contain; border-radius: 2px; }
|
||||
.price-tag { color: #e6a23c; font-weight: bold; }
|
||||
.fee-tag { color: #909399; font-weight: 500; }
|
||||
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
|
||||
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
|
||||
.tag { display: inline-block; padding: 2px 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
|
||||
</style>
|
||||
|
||||
|
||||
13
electron-vue-template/src/renderer/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Vite + Vue template</title>
|
||||
<link rel="icon" href="/icon/icon.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
electron-vue-template/src/renderer/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css';
|
||||
import 'element-plus/dist/index.css'
|
||||
import ElementPlus from 'element-plus'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
89
electron-vue-template/src/renderer/style.css
Normal file
@@ -0,0 +1,89 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
19
electron-vue-template/src/renderer/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"],
|
||||
}
|
||||
|
||||
12
electron-vue-template/src/renderer/typings/electron.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Should match main/preload.ts for typescript support in renderer
|
||||
*/
|
||||
export default interface ElectronApi {
|
||||
sendMessage: (message: string) => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronApi,
|
||||
}
|
||||
}
|
||||
6
electron-vue-template/src/renderer/typings/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
23
electron-vue-template/vite.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const Path = require('path');
|
||||
const vuePlugin = require('@vitejs/plugin-vue')
|
||||
|
||||
const { defineConfig } = require('vite');
|
||||
|
||||
/**
|
||||
* https://vitejs.dev/config
|
||||
*/
|
||||
const config = defineConfig({
|
||||
root: Path.join(__dirname, 'src', 'renderer'),
|
||||
publicDir: Path.join(__dirname, 'public'),
|
||||
server: {
|
||||
port: 8083,
|
||||
},
|
||||
open: false,
|
||||
build: {
|
||||
outDir: Path.join(__dirname, 'build', 'renderer'),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
plugins: [vuePlugin()],
|
||||
});
|
||||
|
||||
module.exports = config;
|
||||
19
erp_client_sb/.claude/settings.local.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mvn:*)",
|
||||
"Bash(del:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthController.java\")",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\login.html\")",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthProxyController.java\")",
|
||||
"Bash(java:*)",
|
||||
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\shopee-platform.html\")",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(\"taskkill\" \"/f\" \"/pid\" \"9804\")",
|
||||
"Bash(dir:*)",
|
||||
"Bash(find:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
1
erp_client_sb/data/device.id
Normal file
@@ -0,0 +1 @@
|
||||
DEVICE-CD31378598644EAA8C0E8153A9D80959
|
||||
BIN
erp_client_sb/data/erp-cache.db
Normal file
185
erp_client_sb/pom.xml
Normal file
@@ -0,0 +1,185 @@
|
||||
<?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/>
|
||||
</parent>
|
||||
<groupId>com.tashow.erp</groupId>
|
||||
<artifactId>erp_client_sb</artifactId>
|
||||
<version>2.4.7</version>
|
||||
<name>erp_client_sb</name>
|
||||
<description>erp客户端</description>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||
<java.version>17</java.version>
|
||||
<springboot.version>3.5.4</springboot.version>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- Apache POI for Excel processing -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>4.1.2</version>
|
||||
</dependency>
|
||||
<!-- 已移除 JavaFX/FxWeaver 相关依赖,保留为纯 Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>com.qiniu</groupId>
|
||||
<artifactId>qiniu-java-sdk</artifactId>
|
||||
<version>7.12.1</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
|
||||
<dependency>
|
||||
<groupId>us.codecraft</groupId>
|
||||
<artifactId>webmagic-core</artifactId>
|
||||
<version>1.0.3</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
|
||||
<dependency>
|
||||
<groupId>us.codecraft</groupId>
|
||||
<artifactId>webmagic-extension</artifactId>
|
||||
<version>1.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFX 相关依赖已移除 -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.36</version>
|
||||
</dependency>
|
||||
<!-- SQLite数据库支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.42.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-community-dialects</artifactId>
|
||||
<version>6.2.7.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>4.23.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>webdrivermanager</artifactId>
|
||||
<version>5.9.2</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.python/jython-standalone -->
|
||||
<dependency>
|
||||
<groupId>org.python</groupId>
|
||||
<artifactId>jython-standalone</artifactId>
|
||||
<version>2.7.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT parsing for local RS256 verification -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>aliyun-repository</id>
|
||||
<name>aliyun repository</name>
|
||||
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>jboss-repository</id>
|
||||
<name>jboss repository</name>
|
||||
<url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>application*.yml</include>
|
||||
<include>application*.properties</include>
|
||||
</includes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<excludes>
|
||||
<exclude>application*.yml</exclude>
|
||||
<exclude>application*.properties</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
<mainClass>com.tashow.erp.ErpClientSbApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- 移除 OpenJFX 打包插件,采用纯 Spring Boot 运行 -->
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
|
After Width: | Height: | Size: 169 KiB |
@@ -0,0 +1,37 @@
|
||||
package com.tashow.erp;
|
||||
|
||||
import com.tashow.erp.utils.ErrorReporter;
|
||||
import com.tashow.erp.utils.ResourcePreloader;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
public class ErpClientSbApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 纯 Spring Boot 启动,不再依赖 JavaFX
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(ErpClientSbApplication.class, args);
|
||||
try {
|
||||
ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class);
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
|
||||
log.error("捕获到未处理异常: " + ex.getMessage(), ex);
|
||||
errorReporter.reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex);
|
||||
});
|
||||
log.info("全局异常处理器已设置");
|
||||
} catch (Exception e) {
|
||||
log.warn("未设置 ErrorReporter,继续启动: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 如需预加载资源,可按需保留
|
||||
try {
|
||||
ResourcePreloader.init();
|
||||
ResourcePreloader.preloadErpDashboard();
|
||||
ResourcePreloader.executePreloading();
|
||||
} catch (Throwable t) {
|
||||
log.warn("资源预加载失败: {}", t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
|
||||
import com.tashow.erp.utils.StringUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 字符集工具类
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class CharsetKit
|
||||
{
|
||||
/** ISO-8859-1 */
|
||||
public static final String ISO_8859_1 = "ISO-8859-1";
|
||||
/** UTF-8 */
|
||||
public static final String UTF_8 = "UTF-8";
|
||||
/** GBK */
|
||||
public static final String GBK = "GBK";
|
||||
/** ISO-8859-1 */
|
||||
public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
|
||||
/** UTF-8 */
|
||||
public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
|
||||
/** GBK */
|
||||
public static final Charset CHARSET_GBK = Charset.forName(GBK);
|
||||
/**
|
||||
* 转换为Charset对象
|
||||
*
|
||||
* @param charset 字符集,为空则返回默认字符集
|
||||
* @return Charset
|
||||
*/
|
||||
public static Charset charset(String charset)
|
||||
{
|
||||
return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换字符串的字符集编码
|
||||
*
|
||||
* @param source 字符串
|
||||
* @param srcCharset 源字符集,默认ISO-8859-1
|
||||
* @param destCharset 目标字符集,默认UTF-8
|
||||
* @return 转换后的字符集
|
||||
*/
|
||||
public static String convert(String source, String srcCharset, String destCharset)
|
||||
{
|
||||
return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换字符串的字符集编码
|
||||
*
|
||||
* @param source 字符串
|
||||
* @param srcCharset 源字符集,默认ISO-8859-1
|
||||
* @param destCharset 目标字符集,默认UTF-8
|
||||
* @return 转换后的字符集
|
||||
*/
|
||||
public static String convert(String source, Charset srcCharset, Charset destCharset)
|
||||
{
|
||||
if (null == srcCharset)
|
||||
{
|
||||
srcCharset = StandardCharsets.ISO_8859_1;
|
||||
}
|
||||
|
||||
if (null == destCharset)
|
||||
{
|
||||
destCharset = StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
return new String(source.getBytes(srcCharset), destCharset);
|
||||
}
|
||||
/**
|
||||
* @return 系统字符集编码
|
||||
*/
|
||||
public static String systemCharset()
|
||||
{
|
||||
return Charset.defaultCharset().name();
|
||||
}
|
||||
}
|
||||
169
erp_client_sb/src/main/java/com/tashow/erp/common/Constants.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 通用常量信息
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class Constants
|
||||
{
|
||||
/**
|
||||
* UTF-8 字符集
|
||||
*/
|
||||
public static final String UTF8 = "UTF-8";
|
||||
|
||||
/**
|
||||
* GBK 字符集
|
||||
*/
|
||||
public static final String GBK = "GBK";
|
||||
|
||||
/**
|
||||
* 系统语言
|
||||
*/
|
||||
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
|
||||
|
||||
/**
|
||||
* www主域
|
||||
*/
|
||||
public static final String WWW = "www.";
|
||||
|
||||
/**
|
||||
* http请求
|
||||
*/
|
||||
public static final String HTTP = "http://";
|
||||
|
||||
/**
|
||||
* https请求
|
||||
*/
|
||||
public static final String HTTPS = "https://";
|
||||
|
||||
/**
|
||||
* 通用成功标识
|
||||
*/
|
||||
public static final String SUCCESS = "0";
|
||||
|
||||
/**
|
||||
* 通用失败标识
|
||||
*/
|
||||
public static final String FAIL = "1";
|
||||
|
||||
/**
|
||||
* 登录成功
|
||||
*/
|
||||
public static final String LOGIN_SUCCESS = "Success";
|
||||
|
||||
/**
|
||||
* 注销
|
||||
*/
|
||||
public static final String LOGOUT = "Logout";
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public static final String REGISTER = "Register";
|
||||
|
||||
/**
|
||||
* 登录失败
|
||||
*/
|
||||
public static final String LOGIN_FAIL = "Error";
|
||||
|
||||
/**
|
||||
* 所有权限标识
|
||||
*/
|
||||
public static final String ALL_PERMISSION = "*:*:*";
|
||||
|
||||
/**
|
||||
* 管理员角色权限标识
|
||||
*/
|
||||
public static final String SUPER_ADMIN = "admin";
|
||||
|
||||
/**
|
||||
* 角色权限分隔符
|
||||
*/
|
||||
public static final String ROLE_DELIMETER = ",";
|
||||
|
||||
/**
|
||||
* 权限标识分隔符
|
||||
*/
|
||||
public static final String PERMISSION_DELIMETER = ",";
|
||||
|
||||
/**
|
||||
* 验证码有效期(分钟)
|
||||
*/
|
||||
public static final Integer CAPTCHA_EXPIRATION = 2;
|
||||
|
||||
/**
|
||||
* 令牌
|
||||
*/
|
||||
public static final String TOKEN = "token";
|
||||
|
||||
/**
|
||||
* 令牌前缀
|
||||
*/
|
||||
public static final String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
/**
|
||||
* 令牌前缀
|
||||
*/
|
||||
public static final String LOGIN_USER_KEY = "login_user_key";
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
public static final String JWT_USERID = "userid";
|
||||
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
public static final String JWT_AVATAR = "avatar";
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
public static final String JWT_CREATED = "created";
|
||||
|
||||
/**
|
||||
* 用户权限
|
||||
*/
|
||||
public static final String JWT_AUTHORITIES = "authorities";
|
||||
|
||||
/**
|
||||
* 资源映射路径 前缀
|
||||
*/
|
||||
public static final String RESOURCE_PREFIX = "/profile";
|
||||
|
||||
/**
|
||||
* RMI 远程方法调用
|
||||
*/
|
||||
public static final String LOOKUP_RMI = "rmi:";
|
||||
|
||||
/**
|
||||
* LDAP 远程方法调用
|
||||
*/
|
||||
public static final String LOOKUP_LDAP = "ldap:";
|
||||
|
||||
/**
|
||||
* LDAPS 远程方法调用
|
||||
*/
|
||||
public static final String LOOKUP_LDAPS = "ldaps:";
|
||||
|
||||
/**
|
||||
* 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
|
||||
*/
|
||||
public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" };
|
||||
|
||||
/**
|
||||
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
|
||||
*/
|
||||
public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" };
|
||||
|
||||
/**
|
||||
* 定时任务违规的字符
|
||||
*/
|
||||
public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
|
||||
"org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" };
|
||||
}
|
||||
1021
erp_client_sb/src/main/java/com/tashow/erp/common/Convert.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.tashow.erp.config;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
/**
|
||||
* 数据库配置,确保数据目录存在
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(1) // 确保在其他组件之前运行
|
||||
public class DatabaseConfig implements ApplicationRunner {
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
// 确保data目录存在
|
||||
Path dataDir = Paths.get("data");
|
||||
if (!Files.exists(dataDir)) {
|
||||
try {
|
||||
Files.createDirectories(dataDir);
|
||||
log.info("创建数据目录: {}", dataDir.toAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
log.error("创建数据目录失败: {}", e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log.info("数据目录已存在: {}", dataDir.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.tashow.erp.config;
|
||||
|
||||
import com.tashow.erp.utils.ErrorReporter;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 错误上报切面 - 自动捕获所有Controller异常并上报,同时检测错误返回值
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class ErrorReportAspect {
|
||||
|
||||
@Autowired
|
||||
private ErrorReporter errorReporter;
|
||||
|
||||
/**
|
||||
* 拦截所有Controller方法,自动上报异常和错误返回值
|
||||
*/
|
||||
@Around("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
|
||||
public Object aroundController(ProceedingJoinPoint point) throws Throwable {
|
||||
String methodName = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
|
||||
|
||||
try {
|
||||
Object result = point.proceed();
|
||||
|
||||
// 检查返回值是否表示错误
|
||||
if (result instanceof JsonData) {
|
||||
JsonData jsonData = (JsonData) result;
|
||||
// code != 0 表示失败(根据JsonData注释:0表示成功,-1表示失败,1表示处理中)
|
||||
if (jsonData.getCode() != null && jsonData.getCode() != 0) {
|
||||
// 创建一个RuntimeException来包装错误信息
|
||||
String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误";
|
||||
Exception syntheticException = new RuntimeException("业务处理失败: " + errorMsg);
|
||||
errorReporter.reportBusinessError(methodName + " 返回错误", syntheticException);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// 自动上报未捕获的异常
|
||||
errorReporter.reportBusinessError(methodName + " 抛出异常", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// package com.tashow.erp.config;
|
||||
//
|
||||
// 已移除 FxWeaver 相关配置(项目改为纯 Spring Boot)。
|
||||
// 如需恢复 JavaFX 集成,请取消注释并恢复依赖。
|
||||
//
|
||||
// import net.rgielen.fxweaver.core.FxWeaver;
|
||||
// import net.rgielen.fxweaver.spring.SpringFxWeaver;
|
||||
// import org.springframework.context.ConfigurableApplicationContext;
|
||||
// import org.springframework.context.annotation.Bean;
|
||||
// import org.springframework.context.annotation.Configuration;
|
||||
//
|
||||
// @Configuration
|
||||
// public class FxWeaverConfig {
|
||||
// @Bean
|
||||
// public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) {
|
||||
// return new SpringFxWeaver(applicationContext);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.tashow.erp.config;
|
||||
|
||||
import com.tashow.erp.fx.controller.JavaBridge;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class JavaBridgeConfig {
|
||||
@Bean
|
||||
public JavaBridge javaBridge() {
|
||||
return new JavaBridge();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// package com.tashow.erp.config;
|
||||
//
|
||||
// 已转为纯 Spring Boot,JavaFX 启动屏幕不再使用。如需恢复,可取消注释并补充 JavaFX 依赖。
|
||||
//
|
||||
// import javafx.application.Platform;
|
||||
// import javafx.scene.Scene;
|
||||
// import javafx.scene.control.ProgressBar;
|
||||
// import javafx.scene.image.Image;
|
||||
// import javafx.scene.image.ImageView;
|
||||
// import javafx.scene.layout.StackPane;
|
||||
// import javafx.scene.layout.VBox;
|
||||
// import javafx.stage.Stage;
|
||||
// import javafx.stage.StageStyle;
|
||||
// import lombok.extern.slf4j.Slf4j;
|
||||
// import org.springframework.stereotype.Component;
|
||||
//
|
||||
// /**
|
||||
// * 自定义启动屏幕
|
||||
// */
|
||||
// @Slf4j
|
||||
// @Component
|
||||
// public class MySplashScreen {
|
||||
// private static final String DEFAULT_IMAGE = "/static/image/splash_screen.png";
|
||||
// private Stage splashStage;
|
||||
// private ProgressBar progressBar;
|
||||
//
|
||||
// public void show() {
|
||||
// Platform.runLater(() -> {
|
||||
// try {
|
||||
// splashStage = new Stage();
|
||||
// splashStage.initStyle(StageStyle.UNDECORATED);
|
||||
// Image splashImage = new Image(getClass().getResourceAsStream(DEFAULT_IMAGE));
|
||||
// ImageView imageView = new ImageView(splashImage);
|
||||
// ProgressBar progressBar = new ProgressBar();
|
||||
// progressBar.setPrefWidth(splashImage.getWidth());
|
||||
// progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
// progressBar.setStyle("-fx-accent: #0078d4; -fx-background-color: transparent;");
|
||||
// StackPane root = new StackPane();
|
||||
// VBox progressContainer = new VBox();
|
||||
// progressContainer.setAlignment(javafx.geometry.Pos.BOTTOM_CENTER);
|
||||
// progressContainer.setSpacing(0);
|
||||
// progressContainer.getChildren().add(progressBar);
|
||||
// root.getChildren().addAll(imageView, progressContainer);
|
||||
// Scene scene = new Scene(root);
|
||||
// splashStage.setScene(scene);
|
||||
// splashStage.setResizable(false);
|
||||
// splashStage.centerOnScreen();
|
||||
// splashStage.show();
|
||||
// } catch (Exception e) {
|
||||
// log.error("显示启动屏幕失败", e);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// public void hide() {
|
||||
// Platform.runLater(() -> {
|
||||
// if (splashStage != null) {
|
||||
// splashStage.hide();
|
||||
// log.info("启动屏幕已隐藏");
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.tashow.erp.config;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Component
|
||||
public class RestTemplateConfig {
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
RestTemplateBuilder builder = new RestTemplateBuilder();
|
||||
builder.connectTimeout(Duration.ofSeconds(5));
|
||||
builder.readTimeout(Duration.ofSeconds(10));
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.tashow.erp.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
* 配置CORS以支持JavaFX WebView的网络请求
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("http://localhost:8083", "http://127.0.0.1:8083", "file://")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.tashow.erp.config;
|
||||
|
||||
import com.tashow.erp.security.LocalJwtAuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private LocalJwtAuthInterceptor localJwtAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localJwtAuthInterceptor)
|
||||
.addPathPatterns("/api/**")
|
||||
.excludePathPatterns(
|
||||
"/api/login",
|
||||
"/api/register",
|
||||
"/api/verify",
|
||||
"/api/check-username");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.tashow.erp.controller;
|
||||
import com.tashow.erp.repository.AmazonProductRepository;
|
||||
import com.tashow.erp.service.IAmazonScrapingService;
|
||||
import com.tashow.erp.utils.ExcelParseUtil;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import com.tashow.erp.utils.LoggerUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@RestController
|
||||
@RequestMapping("/api/amazon")
|
||||
public class AmazonController {
|
||||
private static final Logger logger = LoggerUtil.getLogger(AmazonController.class);
|
||||
@Autowired
|
||||
private IAmazonScrapingService amazonScrapingService;
|
||||
@Autowired
|
||||
private AmazonProductRepository amazonProductRepository;
|
||||
/**
|
||||
* 批量获取亚马逊产品信息
|
||||
*/
|
||||
@PostMapping("/products/batch")
|
||||
public JsonData batchGetProducts(@RequestBody Object request) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> requestMap = (Map<String, Object>) request;
|
||||
List<String> asinList = (List<String>) requestMap.get("asinList");
|
||||
String batchId = (String) requestMap.get("batchId");
|
||||
return JsonData.buildSuccess(amazonScrapingService.batchGetProductInfo(asinList, batchId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新产品数据
|
||||
*/
|
||||
@GetMapping("/products/latest")
|
||||
public JsonData getLatestProducts() {
|
||||
List<Map<String, Object>> products = amazonProductRepository.findLatestProducts()
|
||||
.parallelStream()
|
||||
.map(entity -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("asin", entity.getAsin());
|
||||
map.put("title", entity.getTitle());
|
||||
map.put("price", entity.getPrice());
|
||||
map.put("imageUrl", entity.getImageUrl());
|
||||
map.put("productUrl", entity.getProductUrl());
|
||||
map.put("brand", entity.getBrand());
|
||||
map.put("category", entity.getCategory());
|
||||
map.put("rating", entity.getRating());
|
||||
map.put("reviewCount", entity.getReviewCount());
|
||||
map.put("availability", entity.getAvailability());
|
||||
map.put("seller", entity.getSeller());
|
||||
map.put("shipper", entity.getSeller());
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("products", products);
|
||||
result.put("total", products.size());
|
||||
return JsonData.buildSuccess(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Excel文件获取ASIN列表
|
||||
*/
|
||||
@PostMapping("/import/asin")
|
||||
public JsonData importAsinFromExcel(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return JsonData.buildError("上传文件为空");
|
||||
}
|
||||
try {
|
||||
List<String> asinList = ExcelParseUtil.parseFirstColumn(file);
|
||||
if (asinList.isEmpty()) {
|
||||
return JsonData.buildError("未从文件中解析到ASIN数据");
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("asinList", asinList);
|
||||
result.put("total", asinList.size());
|
||||
return JsonData.buildSuccess(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("解析文件失败: {}", e.getMessage(), e);
|
||||
return JsonData.buildError("解析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.tashow.erp.controller;
|
||||
import com.tashow.erp.entity.AuthTokenEntity;
|
||||
import com.tashow.erp.entity.CacheDataEntity;
|
||||
import com.tashow.erp.repository.AuthTokenRepository;
|
||||
import com.tashow.erp.repository.CacheDataRepository;
|
||||
import com.tashow.erp.service.IAuthService;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AuthController {
|
||||
@Autowired
|
||||
private IAuthService authService;
|
||||
@Autowired
|
||||
private AuthTokenRepository authTokenRepository;
|
||||
@Autowired
|
||||
private CacheDataRepository cacheDataRepository;
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody Map<String, Object> loginData) {
|
||||
String username = (String) loginData.get("username");
|
||||
String password = (String) loginData.get("password");
|
||||
if (username == null || password == null) {
|
||||
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
|
||||
}
|
||||
Map<String, Object> result = authService.login(username, password);
|
||||
Object success = result.get("success");
|
||||
Object tokenObj = result.get("token");
|
||||
if (Boolean.TRUE.equals(success) && tokenObj instanceof String token && token != null && !token.isEmpty()) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
|
||||
return ResponseEntity.ok().headers(headers).body(result);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ResponseEntity<?> verifyToken(@RequestBody Map<String, Object> data) {
|
||||
String token = (String) data.get("token");
|
||||
if (token == null) {
|
||||
return ResponseEntity.ok(Map.of("code", 400, "message", "token不能为空"));
|
||||
}
|
||||
Map<String, Object> result = authService.verifyToken(token);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> register(@RequestBody Map<String, Object> registerData) {
|
||||
String username = (String) registerData.get("username");
|
||||
String password = (String) registerData.get("password");
|
||||
if (username == null || password == null) {
|
||||
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
|
||||
}
|
||||
Map<String, Object> result = authService.register(username, password);
|
||||
Object success2 = result.get("success");
|
||||
Object tokenObj2 = result.get("token");
|
||||
if (Boolean.TRUE.equals(success2) && tokenObj2 instanceof String token && token != null && !token.isEmpty()) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
|
||||
return ResponseEntity.ok().headers(headers).body(result);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/check-username")
|
||||
public ResponseEntity<?> checkUsername(@RequestParam String username) {
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名不能为空"));
|
||||
}
|
||||
boolean available = authService.checkUsername(username);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"code", 200,
|
||||
"message", "检查成功",
|
||||
"data", available
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存认证密钥
|
||||
*/
|
||||
@PostMapping("/auth/save")
|
||||
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
|
||||
String serviceName = (String) data.get("serviceName");
|
||||
String authKey = (String) data.get("authKey");
|
||||
|
||||
if (serviceName == null || authKey == null) return JsonData.buildError("serviceName和authKey不能为空");
|
||||
|
||||
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName).orElse(new AuthTokenEntity());
|
||||
entity.setServiceName(serviceName);
|
||||
entity.setToken(authKey);
|
||||
authTokenRepository.save(entity);
|
||||
|
||||
return JsonData.buildSuccess("认证信息保存成功");
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/auth/get")
|
||||
public JsonData getAuth(@RequestParam String serviceName) {
|
||||
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除认证密钥
|
||||
*/
|
||||
@DeleteMapping("/auth/remove")
|
||||
public JsonData removeAuth(@RequestParam String serviceName) {
|
||||
authTokenRepository.findByServiceName(serviceName).ifPresent(authTokenRepository::delete);
|
||||
return JsonData.buildSuccess("认证信息删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存缓存数据
|
||||
*/
|
||||
@PostMapping("/cache/save")
|
||||
public JsonData saveCache(@RequestBody Map<String, Object> data) {
|
||||
String key = (String) data.get("key");
|
||||
String value = (String) data.get("value");
|
||||
|
||||
if (key == null || value == null) return JsonData.buildError("key和value不能为空");
|
||||
|
||||
CacheDataEntity entity = cacheDataRepository.findByCacheKey(key).orElse(new CacheDataEntity());
|
||||
entity.setCacheKey(key);
|
||||
entity.setCacheValue(value);
|
||||
cacheDataRepository.save(entity);
|
||||
|
||||
return JsonData.buildSuccess("缓存数据保存成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存数据
|
||||
*/
|
||||
@GetMapping("/cache/get")
|
||||
public JsonData getCache(@RequestParam String key) {
|
||||
return JsonData.buildSuccess(cacheDataRepository.findByCacheKey(key)
|
||||
.map(CacheDataEntity::getCacheValue).orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存数据
|
||||
*/
|
||||
@DeleteMapping("/cache/remove")
|
||||
public JsonData removeCache(@RequestParam String key) {
|
||||
cacheDataRepository.findByCacheKey(key).ifPresent(cacheDataRepository::delete);
|
||||
return JsonData.buildSuccess("缓存数据删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存数据 - POST方式
|
||||
*/
|
||||
@PostMapping("/cache/delete")
|
||||
public JsonData deleteCacheByPost(@RequestParam String key) {
|
||||
if (key == null || key.trim().isEmpty()) {
|
||||
return JsonData.buildError("key不能为空");
|
||||
}
|
||||
cacheDataRepository.deleteByCacheKey(key);
|
||||
return JsonData.buildSuccess("缓存数据删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话引导:检查SQLite中是否存在token
|
||||
*/
|
||||
@GetMapping("/session/bootstrap")
|
||||
public ResponseEntity<?> sessionBootstrap() {
|
||||
Optional<CacheDataEntity> tokenEntity = cacheDataRepository.findByCacheKey("token");
|
||||
if (tokenEntity.isEmpty()) {
|
||||
return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录"));
|
||||
}
|
||||
String token = tokenEntity.get().getCacheValue();
|
||||
if (token == null || token.isEmpty()) {
|
||||
return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录"));
|
||||
}
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
|
||||
return ResponseEntity.ok().headers(headers).body(Map.of("code", 200, "message", "会话已恢复"));
|
||||
}
|
||||
|
||||
private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(name).append("=").append(value).append(";");
|
||||
sb.append(" Path=/;");
|
||||
sb.append(" HttpOnly;");
|
||||
sb.append(" SameSite=Strict;");
|
||||
if (maxAgeSeconds > 0) {
|
||||
sb.append(" Max-Age=").append(maxAgeSeconds).append(";");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.tashow.erp.controller;
|
||||
import com.tashow.erp.fx.controller.JavaBridge;
|
||||
import com.tashow.erp.repository.BanmaOrderRepository;
|
||||
import com.tashow.erp.service.IBanmaOrderService;
|
||||
import com.tashow.erp.utils.ExcelExportUtil;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import com.tashow.erp.utils.LoggerUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@RestController
|
||||
@RequestMapping("/api/banma")
|
||||
public class BanmaOrderController {
|
||||
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderController.class);
|
||||
@Autowired
|
||||
IBanmaOrderService banmaOrderService;
|
||||
@Autowired
|
||||
BanmaOrderRepository banmaOrderRepository;
|
||||
@Autowired
|
||||
JavaBridge javaBridge;
|
||||
@Autowired
|
||||
RestTemplate restTemplate;
|
||||
@GetMapping("/orders")
|
||||
public ResponseEntity<Map<String, Object>> getOrders(
|
||||
@RequestParam(required = false, name = "startDate") String startDate,
|
||||
@RequestParam(required = false, name = "endDate") String endDate,
|
||||
@RequestParam(defaultValue = "1", name = "page") int page,
|
||||
@RequestParam(defaultValue = "10", name = "pageSize") int pageSize,
|
||||
@RequestParam(required = false, name = "batchId") String batchId,
|
||||
@RequestParam(required = false, name = "shopIds") String shopIds) {
|
||||
List<String> shopIdList = shopIds != null ? java.util.Arrays.asList(shopIds.split(",")) : null;
|
||||
Map<String, Object> result = banmaOrderService.getOrdersByPage(startDate, endDate, page, pageSize, batchId, shopIdList);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
/**
|
||||
* 获取店铺列表
|
||||
*/
|
||||
@GetMapping("/shops")
|
||||
public JsonData getShops() {
|
||||
try {
|
||||
Map<String, Object> response = banmaOrderService.getShops();
|
||||
return JsonData.buildSuccess(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取店铺列表失败: {}", e.getMessage(), e);
|
||||
return JsonData.buildError("获取店铺列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新斑马认证Token
|
||||
*/
|
||||
@PostMapping("/refresh-token")
|
||||
public JsonData refreshToken(){
|
||||
try {
|
||||
banmaOrderService.refreshToken();
|
||||
return JsonData.buildSuccess("Token刷新成功");
|
||||
} catch (Exception e) {
|
||||
logger.error("刷新Token失败: {}", e.getMessage(), e);
|
||||
return JsonData.buildError("Token刷新失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取最新订单数据
|
||||
*/
|
||||
@GetMapping("/orders/latest")
|
||||
public JsonData getLatestOrders() {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
List<Map<String, Object>> orders = banmaOrderRepository.findLatestOrders()
|
||||
.parallelStream()
|
||||
.map(entity -> {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = mapper.readValue(entity.getOrderData(), Map.class);
|
||||
return data;
|
||||
} catch (Exception e) {
|
||||
return new HashMap<String, Object>();
|
||||
}
|
||||
})
|
||||
.filter(order -> !order.isEmpty())
|
||||
.toList();
|
||||
|
||||
return JsonData.buildSuccess(Map.of("orders", orders, "total", orders.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaFX专用:导出并保存Excel文件到桌面
|
||||
*/
|
||||
@PostMapping("/export-and-save")
|
||||
public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> orders = (List<Map<String, Object>>) body.get("orders");
|
||||
String[] headers = {"下单时间", "商品图片", "商品名称", "乐天订单号", "下单距今时间", "乐天订单金额/日元",
|
||||
"购买数量", "税费/日元", "服务商回款抽点rmb", "商品番号", "1688采购订单号",
|
||||
"采购金额/rmb", "国际运费/rmb", "国内物流公司", "国内物流单号", "日本物流单号", "地址状态"};
|
||||
|
||||
byte[] excelData = ExcelExportUtil.createExcelWithImages("斑马订单数据", headers, orders, 1, "productImage");
|
||||
if (excelData.length == 0) return JsonData.buildError("生成Excel文件失败");
|
||||
|
||||
String fileName = String.format("斑马订单数据_%s.xlsx", java.time.LocalDate.now().toString());
|
||||
String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName);
|
||||
|
||||
return savedPath != null
|
||||
? JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName))
|
||||
: JsonData.buildError("保存文件失败,请检查权限");
|
||||
} catch (Exception e) {
|
||||
logger.error("导出并保存斑马订单Excel失败: {}", e.getMessage(), e);
|
||||
return JsonData.buildError("导出并保存Excel失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.tashow.erp.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 配置信息控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/config")
|
||||
public class ConfigController {
|
||||
|
||||
@Value("${api.server.base-url}")
|
||||
private String serverBaseUrl;
|
||||
|
||||
/**
|
||||
* 获取服务器配置
|
||||
*/
|
||||
@GetMapping("/server")
|
||||
public Map<String, Object> getServerConfig() {
|
||||
return Map.of(
|
||||
"baseUrl", serverBaseUrl,
|
||||
"sseUrl", serverBaseUrl + "/monitor/account/events"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.tashow.erp.controller;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import com.tashow.erp.utils.ApiForwarder;
|
||||
import com.tashow.erp.utils.DeviceUtils;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 设备管理代理控制器
|
||||
* 简化职责:透传请求到后端服务
|
||||
*/
|
||||
@RestController
|
||||
public class DeviceProxyController {
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Value("${api.server.base-url}")
|
||||
private String serverBaseUrl;
|
||||
|
||||
/**
|
||||
* 注册设备
|
||||
*/
|
||||
@Autowired
|
||||
private ApiForwarder apiForwarder;
|
||||
|
||||
@PostMapping("/api/device/register")
|
||||
public ResponseEntity<?> deviceRegister(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
||||
Map<String, Object> deviceData = new HashMap<>(body);
|
||||
deviceData.put("deviceId", DeviceUtils.generateDeviceId());
|
||||
return apiForwarder.post("/monitor/device/register", deviceData, auth);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostMapping("/api/device/remove")
|
||||
public ResponseEntity<?> deviceRemove(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
||||
return apiForwarder.post("/monitor/device/remove", body, auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备心跳
|
||||
*/
|
||||
@PostMapping("/api/device/heartbeat")
|
||||
public ResponseEntity<?> deviceHeartbeat(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
||||
return apiForwarder.post("/monitor/device/heartbeat", body, auth);
|
||||
}
|
||||
|
||||
@GetMapping("/api/device/quota")
|
||||
public ResponseEntity<?> deviceQuota(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
|
||||
return apiForwarder.get("/monitor/device/quota?username=" + username, auth);
|
||||
}
|
||||
|
||||
@GetMapping("/api/device/list")
|
||||
public ResponseEntity<?> deviceList(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
|
||||
return apiForwarder.get("/monitor/device/list?username=" + username, auth);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.tashow.erp.controller;
|
||||
|
||||
import com.tashow.erp.service.IGenmaiService;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import com.tashow.erp.utils.LoggerUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/genmai")
|
||||
public class GenmaiController {
|
||||
private static final Logger logger = LoggerUtil.getLogger(GenmaiController.class);
|
||||
|
||||
@Autowired
|
||||
private IGenmaiService genmaiService;
|
||||
|
||||
/**
|
||||
* 打开跟卖精灵网页
|
||||
*/
|
||||
@PostMapping("/open")
|
||||
public void openGenmaiWebsite() {
|
||||
genmaiService.openGenmaiWebsite();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.tashow.erp.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home() {
|
||||
return "redirect:/html/erp-dashboard.html";
|
||||
}
|
||||
|
||||
@GetMapping("/erp")
|
||||
public String erp() {
|
||||
return "redirect:/html/erp-dashboard.html";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.tashow.erp.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 代理控制器,用于解决CORS跨域问题
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/proxy")
|
||||
public class ProxyController {
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
/**
|
||||
* 代理获取图片
|
||||
* @param requestBody 包含图片URL的请求体
|
||||
* @return 图片字节数组
|
||||
*/
|
||||
@PostMapping("/image")
|
||||
public ResponseEntity<byte[]> proxyImage(@RequestBody Map<String, String> requestBody) {
|
||||
String imageUrl = requestBody.get("imageUrl");
|
||||
if (imageUrl == null || imageUrl.isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
|
||||
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
// 发送请求获取图片
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||
imageUrl,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
byte[].class
|
||||
);
|
||||
|
||||
// 设置响应头
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
|
||||
|
||||
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过URL参数代理获取图片(为JavaFX WebView优化)
|
||||
* @param imageUrl 图片URL
|
||||
* @return 图片字节数组
|
||||
*/
|
||||
@GetMapping("/image-url")
|
||||
public ResponseEntity<byte[]> proxyImageByUrl(@RequestParam("url") String imageUrl) {
|
||||
if (imageUrl == null || imageUrl.isEmpty()) {
|
||||
System.err.println("图片代理请求失败: 图片URL为空");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
System.out.println("代理图片请求: " + imageUrl);
|
||||
|
||||
try {
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
|
||||
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
|
||||
headers.set("Referer", "https://item.rakuten.co.jp/");
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
// 发送请求获取图片
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||
imageUrl,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
byte[].class
|
||||
);
|
||||
|
||||
System.out.println("图片代理成功,响应大小: " + (response.getBody() != null ? response.getBody().length : 0) + " bytes");
|
||||
|
||||
// 设置响应头,支持缓存以提升JavaFX WebView性能
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
|
||||
// 尝试从原始响应中获取Content-Type
|
||||
String contentType = response.getHeaders().getFirst("Content-Type");
|
||||
if (contentType != null && contentType.startsWith("image/")) {
|
||||
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
|
||||
} else {
|
||||
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
|
||||
}
|
||||
|
||||
// 设置缓存头以提升性能
|
||||
responseHeaders.setCacheControl("max-age=3600");
|
||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||
|
||||
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
|
||||
} catch (Exception e) {
|
||||
System.err.println("图片代理失败: " + imageUrl + " - " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.tashow.erp.controller;
|
||||
|
||||
import com.tashow.erp.model.RakutenProduct;
|
||||
import com.tashow.erp.model.SearchResult;
|
||||
import com.tashow.erp.repository.RakutenProductRepository;
|
||||
import com.tashow.erp.service.Alibaba1688Service;
|
||||
import com.tashow.erp.service.IRakutenCacheService;
|
||||
import com.tashow.erp.service.RakutenScrapingService;
|
||||
import com.tashow.erp.service.impl.Alibaba1688ServiceImpl;
|
||||
import com.tashow.erp.utils.DataReportUtil;
|
||||
import com.tashow.erp.utils.ExcelParseUtil;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import com.tashow.erp.utils.QiniuUtil;
|
||||
import com.tashow.erp.fx.controller.JavaBridge;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Base64;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/rakuten")
|
||||
@Slf4j
|
||||
public class RakutenController {
|
||||
@Autowired
|
||||
private RakutenScrapingService rakutenScrapingService;
|
||||
@Autowired
|
||||
private Alibaba1688Service alibaba1688Service;
|
||||
@Autowired
|
||||
private IRakutenCacheService rakutenCacheService;
|
||||
@Autowired
|
||||
private JavaBridge javaBridge;
|
||||
@Autowired
|
||||
private DataReportUtil dataReportUtil;
|
||||
/**
|
||||
* 获取乐天商品数据(支持单个店铺名或 Excel 文件上传)
|
||||
*
|
||||
* @param file 可选,Excel 文件(首列为店铺名)
|
||||
* @param shopName 可选,单个店铺名
|
||||
* @param batchId 可选,批次号
|
||||
* @return JsonData 响应
|
||||
*/
|
||||
@PostMapping(value = "/products")
|
||||
public JsonData getProducts(@RequestParam(value = "file", required = false) MultipartFile file, @RequestParam(value = "shopName", required = false) String shopName, @RequestParam(value = "batchId", required = false) String batchId) {
|
||||
try {
|
||||
// 1. 获取店铺名集合(优先 shopName,其次 Excel)
|
||||
List<String> shopNames = Optional.ofNullable(shopName).filter(s -> !s.trim().isEmpty()).map(s -> List.of(s.trim())).orElseGet(() -> file != null ? ExcelParseUtil.parseFirstColumn(file) : new ArrayList<>());
|
||||
|
||||
if (CollectionUtils.isEmpty(shopNames)) {
|
||||
return JsonData.buildError("未从 Excel 中解析到店铺名,且 shopName 参数为空");
|
||||
}
|
||||
|
||||
List<RakutenProduct> allProducts = new ArrayList<>();
|
||||
List<String> skippedShops = new ArrayList<>();
|
||||
|
||||
// 2. 遍历店铺,优先缓存,缺失则爬取
|
||||
for (String currentShopName : shopNames) {
|
||||
if (rakutenCacheService.hasRecentData(currentShopName)) {
|
||||
// 从缓存获取
|
||||
List<RakutenProduct> cached = rakutenCacheService.getProductsByShopName(currentShopName).stream().filter(p -> currentShopName.equals(p.getOriginalShopName())).toList();
|
||||
rakutenCacheService.updateSpecificProductsSessionId(cached, batchId);
|
||||
allProducts.addAll(cached);
|
||||
skippedShops.add(currentShopName);
|
||||
log.info("使用缓存数据,店铺: {},数量: {}", currentShopName, cached.size());
|
||||
} else {
|
||||
// 爬取新数据
|
||||
log.info("采集新数据: {}", currentShopName);
|
||||
List<RakutenProduct> fresh = rakutenScrapingService.scrapeProductsWithSearch(currentShopName);
|
||||
fresh.forEach(p -> p.setOriginalShopName(currentShopName));
|
||||
allProducts.addAll(fresh);
|
||||
log.info("采集完成: {}, 数量: {}", currentShopName, fresh.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 处理新采集的数据,存储并生成 sessionId
|
||||
List<RakutenProduct> newProducts = allProducts.stream().filter(p -> !skippedShops.contains(p.getOriginalShopName())).toList();
|
||||
|
||||
if (!newProducts.isEmpty()) {
|
||||
// 使用已有 sessionId 保存
|
||||
rakutenCacheService.saveProductsWithSessionId(newProducts, batchId);
|
||||
}
|
||||
// 4. 上报缓存数据使用情况
|
||||
int cachedCount = allProducts.size() - newProducts.size();
|
||||
if (cachedCount > 0) {
|
||||
dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0");
|
||||
}
|
||||
|
||||
// 5. 如果是单店铺查询,只返回该店铺的商品
|
||||
List<RakutenProduct> finalProducts = (shopName != null && !shopName.trim().isEmpty()) ? allProducts.stream().filter(p -> shopName.trim().equals(p.getOriginalShopName())).toList() : allProducts;
|
||||
|
||||
return JsonData.buildSuccess(Map.of("products", finalProducts, "total", finalProducts.size(), "sessionId", batchId, "skippedShops", skippedShops, "newProductsCount", newProducts.size()));
|
||||
} catch (Exception e) {
|
||||
log.error("获取乐天商品失败", e);
|
||||
return JsonData.buildError("获取乐天商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1688识图搜索API - 自动保存1688搜索结果
|
||||
*/
|
||||
@PostMapping("/search1688")
|
||||
public JsonData search1688(@RequestBody Map<String, Object> params) {
|
||||
String imageUrl = (String) params.get("imageUrl");
|
||||
String sessionId = (String) params.get("sessionId");
|
||||
try {
|
||||
SearchResult result = alibaba1688Service.get1688Detail(imageUrl);
|
||||
rakutenScrapingService.update1688DataByImageUrl(result, sessionId, imageUrl);
|
||||
return JsonData.buildSuccess(result);
|
||||
} catch (Exception e) {
|
||||
log.error("1688识图搜索失败", e);
|
||||
return JsonData.buildError("搜索失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/products/latest")
|
||||
public JsonData getLatestProducts() {
|
||||
try {
|
||||
List<Map<String, Object>> products = rakutenScrapingService.getLatestProductsForDisplay();
|
||||
return JsonData.buildSuccess(Map.of("products", products, "total", products.size()));
|
||||
} catch (Exception e) {
|
||||
|
||||
e.printStackTrace();
|
||||
log.info("获取最新商品数据失败", e);
|
||||
return JsonData.buildError("获取最新数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@PostMapping("/export-and-save")
|
||||
public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked") List<Map<String, Object>> products = (List<Map<String, Object>>) body.get("products");
|
||||
if (CollectionUtils.isEmpty(products)) return JsonData.buildError("没有可导出的数据");
|
||||
|
||||
boolean skipImages = Optional.ofNullable((Boolean) body.get("skipImages")).orElse(false);
|
||||
String fileName = Optional.ofNullable((String) body.get("fileName")).filter(name -> !name.trim().isEmpty()).orElse("乐天商品数据_" + java.time.LocalDate.now() + ".xlsx");
|
||||
|
||||
String[] headers = {"店铺名", "商品图片", "商品链接", "排名", "商品标题", "价格", "1688识图链接", "1688价格", "1688重量"};
|
||||
byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages("乐天商品数据", headers, products, skipImages ? -1 : 1, skipImages ? null : "imgUrl");
|
||||
|
||||
if (excelData == null || excelData.length == 0) return JsonData.buildError("生成Excel失败");
|
||||
|
||||
String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName);
|
||||
if (savedPath == null) return JsonData.buildError("保存文件失败");
|
||||
|
||||
log.info("导出Excel: {}, 记录数: {}", fileName, products.size());
|
||||
return JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName, "recordCount", products.size(), "hasImages", !skipImages));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("导出Excel失败", e);
|
||||
return JsonData.buildError("导出Excel失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
package com.tashow.erp.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.tashow.erp.entity.UpdateStatusEntity;
|
||||
import com.tashow.erp.repository.UpdateStatusRepository;
|
||||
import com.tashow.erp.service.IAuthService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Web版本更新控制器
|
||||
*
|
||||
* @author Claude
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/update")
|
||||
public class UpdateController {
|
||||
@Value("${project.version:2.3.6}")
|
||||
private String currentVersion;
|
||||
@Value("${project.build.time:}")
|
||||
private String buildTime;
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
@Autowired
|
||||
private IAuthService authService;
|
||||
|
||||
@Autowired
|
||||
private UpdateStatusRepository updateStatusRepository;
|
||||
|
||||
// 下载进度跟踪
|
||||
private volatile int downloadProgress = 0;
|
||||
private volatile long downloadedBytes = 0;
|
||||
private volatile long totalBytes = 0;
|
||||
private volatile String downloadStatus = "ready"; // ready, downloading, completed, failed, cancelled
|
||||
private volatile String downloadSpeed = "0 KB/s";
|
||||
private volatile long downloadStartTime = 0;
|
||||
private volatile boolean downloadCancelled = false;
|
||||
|
||||
/**
|
||||
* 获取当前版本号
|
||||
*
|
||||
* @return 当前版本号
|
||||
*/
|
||||
@GetMapping("/version")
|
||||
public Map<String, Object> getVersion() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("currentVersion", currentVersion);
|
||||
result.put("buildTime", buildTime);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查版本更新
|
||||
*
|
||||
* @return 版本更新信息
|
||||
*/
|
||||
@GetMapping("/check")
|
||||
public Map<String, Object> checkUpdate() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
String response = authService.checkVersion(currentVersion);
|
||||
// 解析JSON响应
|
||||
JsonNode responseNode = objectMapper.readTree(response);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("currentVersion", currentVersion);
|
||||
|
||||
// 检查响应格式并提取数据
|
||||
JsonNode dataNode = null;
|
||||
dataNode = responseNode.get("data");
|
||||
|
||||
// 直接使用服务器返回的needUpdate字段
|
||||
boolean needUpdate = dataNode.has("needUpdate") &&
|
||||
dataNode.get("needUpdate").asBoolean();
|
||||
// 添加跳过版本信息,让前端自己判断
|
||||
String skippedVersion = updateStatusRepository.findByKeyName("skippedUpdateVersion")
|
||||
.map(entity -> entity.getValueData()).orElse(null);
|
||||
result.put("skippedVersion", skippedVersion);
|
||||
|
||||
result.put("needUpdate", needUpdate);
|
||||
|
||||
// 获取最新版本号
|
||||
if (dataNode.has("latestVersion")) {
|
||||
result.put("latestVersion", dataNode.get("latestVersion").asText());
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
if (dataNode.has("downloadUrl")) {
|
||||
String downloadUrl = dataNode.get("downloadUrl").asText();
|
||||
result.put("downloadUrl", downloadUrl);
|
||||
saveUpdateInfo("downloadUrl", downloadUrl);
|
||||
}
|
||||
if (dataNode.has("updateTime")) {
|
||||
// 转换时间戳为可读格式
|
||||
long updateTime = dataNode.get("updateTime").asLong();
|
||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
String releaseDate = sdf.format(new java.util.Date(updateTime));
|
||||
result.put("releaseDate", releaseDate);
|
||||
saveUpdateInfo("releaseDate", releaseDate);
|
||||
}
|
||||
if (dataNode.has("updateNotes")) {
|
||||
String updateNotes = dataNode.get("updateNotes").asText();
|
||||
result.put("updateNotes", updateNotes);
|
||||
saveUpdateInfo("updateNotes", updateNotes);
|
||||
}
|
||||
if (dataNode.has("fileSize")) {
|
||||
String fileSize = dataNode.get("fileSize").asText();
|
||||
result.put("fileSize", fileSize);
|
||||
saveUpdateInfo("fileSize", fileSize);
|
||||
}
|
||||
if (dataNode.has("latestVersion")) {
|
||||
String latestVersion = dataNode.get("latestVersion").asText();
|
||||
saveUpdateInfo("latestVersion", latestVersion);
|
||||
}
|
||||
} else {
|
||||
// 如果不需要更新,清理之前保存的更新信息
|
||||
clearUpdateInfo();
|
||||
}
|
||||
|
||||
// 从SQLite读取之前保存的更新信息
|
||||
result.putAll(getStoredUpdateInfo());
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "版本检查失败:" + e.getMessage());
|
||||
authService.reportError("UPDATE_CHECK_ERROR", "Web版本检查失败", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新信息到SQLite
|
||||
*/
|
||||
private void saveUpdateInfo(String key, String value) {
|
||||
try {
|
||||
UpdateStatusEntity entity = updateStatusRepository.findByKeyName(key)
|
||||
.orElse(new UpdateStatusEntity());
|
||||
entity.setKeyName(key);
|
||||
entity.setValueData(value);
|
||||
updateStatusRepository.save(entity);
|
||||
} catch (Exception e) {
|
||||
System.err.println("保存更新信息失败: " + key + " = " + value + ", 错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SQLite获取存储的更新信息
|
||||
*/
|
||||
private Map<String, Object> getStoredUpdateInfo() {
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
try {
|
||||
String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"};
|
||||
for (String key : keys) {
|
||||
updateStatusRepository.findByKeyName(key).ifPresent(entity ->
|
||||
info.put(key, entity.getValueData())
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("读取更新信息失败: " + e.getMessage());
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理更新信息
|
||||
*/
|
||||
private void clearUpdateInfo() {
|
||||
try {
|
||||
String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"};
|
||||
for (String key : keys) {
|
||||
updateStatusRepository.findByKeyName(key).ifPresent(entity ->
|
||||
updateStatusRepository.delete(entity)
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("清理更新信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取下载进度
|
||||
*
|
||||
* @return 下载进度信息
|
||||
*/
|
||||
@GetMapping("/progress")
|
||||
public Map<String, Object> getDownloadProgress() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("progress", downloadProgress);
|
||||
result.put("status", downloadStatus);
|
||||
result.put("downloadedBytes", downloadedBytes);
|
||||
result.put("totalBytes", totalBytes);
|
||||
result.put("downloadSpeed", downloadSpeed);
|
||||
result.put("downloadedMB", String.format("%.1f", downloadedBytes / 1024.0 / 1024.0));
|
||||
result.put("totalMB", String.format("%.1f", totalBytes / 1024.0 / 1024.0));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置下载状态
|
||||
*
|
||||
* @return 重置结果
|
||||
*/
|
||||
@PostMapping("/reset")
|
||||
public Map<String, Object> resetDownloadStatus() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
downloadProgress = 0;
|
||||
downloadedBytes = 0;
|
||||
totalBytes = 0;
|
||||
downloadStatus = "ready";
|
||||
downloadSpeed = "0 KB/s";
|
||||
downloadStartTime = 0;
|
||||
downloadCancelled = false;
|
||||
tempUpdateFilePath = null;
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", "下载状态已重置");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下载
|
||||
*
|
||||
* @return 取消结果
|
||||
*/
|
||||
@PostMapping("/cancel")
|
||||
public Map<String, Object> cancelDownload() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if ("downloading".equals(downloadStatus)) {
|
||||
downloadCancelled = true;
|
||||
downloadStatus = "cancelled";
|
||||
downloadSpeed = "已取消";
|
||||
result.put("success", true);
|
||||
result.put("message", "下载已取消");
|
||||
} else if ("completed".equals(downloadStatus)) {
|
||||
// 下载完成时点击取消,不删除文件,只是标记为稍后更新
|
||||
result.put("success", true);
|
||||
result.put("message", "已设置为稍后更新,文件保留");
|
||||
System.out.println("用户选择稍后更新,文件路径: " + tempUpdateFilePath);
|
||||
} else {
|
||||
result.put("success", false);
|
||||
result.put("message", "无效的操作状态");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全清除更新文件和状态
|
||||
*
|
||||
* @return 清除结果
|
||||
*/
|
||||
@PostMapping("/clear")
|
||||
public Map<String, Object> clearUpdateFiles() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (tempUpdateFilePath != null) {
|
||||
try {
|
||||
java.io.File tempFile = new java.io.File(tempUpdateFilePath);
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
System.out.println("已删除更新文件: " + tempUpdateFilePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("删除临时文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
downloadProgress = 0;
|
||||
downloadedBytes = 0;
|
||||
totalBytes = 0;
|
||||
downloadStatus = "ready";
|
||||
downloadSpeed = "0 KB/s";
|
||||
downloadCancelled = false;
|
||||
tempUpdateFilePath = null;
|
||||
result.put("success", true);
|
||||
result.put("message", "更新文件和状态已清除");
|
||||
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 验证更新文件是否存在
|
||||
*
|
||||
* @return 验证结果
|
||||
*/
|
||||
@GetMapping("/verify-file")
|
||||
public Map<String, Object> verifyUpdateFile() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
boolean fileExists = false;
|
||||
String filePath = "";
|
||||
|
||||
if (tempUpdateFilePath != null) {
|
||||
java.io.File updateFile = new java.io.File(tempUpdateFilePath);
|
||||
fileExists = updateFile.exists();
|
||||
filePath = tempUpdateFilePath;
|
||||
|
||||
if (fileExists) {
|
||||
result.put("fileSize", updateFile.length());
|
||||
result.put("lastModified", new java.util.Date(updateFile.lastModified()));
|
||||
}
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("fileExists", fileExists);
|
||||
result.put("filePath", filePath);
|
||||
result.put("downloadStatus", downloadStatus);
|
||||
|
||||
System.out.println("验证更新文件: " + filePath + ", 存在: " + fileExists);
|
||||
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "验证文件失败:" + e.getMessage());
|
||||
result.put("fileExists", false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户确认后执行安装
|
||||
*
|
||||
* @return 安装结果
|
||||
*/
|
||||
@PostMapping("/install")
|
||||
public Map<String, Object> installUpdate() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (tempUpdateFilePath == null || !new java.io.File(tempUpdateFilePath).exists()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "更新文件不存在,请重新下载");
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
result.put("success", true);
|
||||
result.put("message", "开始安装更新...");
|
||||
|
||||
// 异步执行安装,避免阻塞响应
|
||||
new Thread(() -> {
|
||||
String updateScript = createUpdateScript(null, tempUpdateFilePath);
|
||||
if (updateScript != null) {
|
||||
executeUpdateAndExit(updateScript);
|
||||
}
|
||||
}).start();
|
||||
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "启动安装失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动更新:下载、替换、重启
|
||||
*
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PostMapping("/auto-update")
|
||||
public Map<String, Object> autoUpdate(@RequestBody Map<String, String> request) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
String downloadUrl = request.get("downloadUrl");
|
||||
result.put("success", true);
|
||||
result.put("message", "开始自动更新...");
|
||||
result.put("downloadUrl", downloadUrl);
|
||||
String finalDownloadUrl = downloadUrl;
|
||||
new Thread(() -> {
|
||||
performUpdate(finalDownloadUrl);
|
||||
}).start();
|
||||
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "启动更新失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新过程
|
||||
*/
|
||||
private void performUpdate(String downloadUrl) {
|
||||
String tempUpdateFile = downloadUpdate(downloadUrl);
|
||||
if (tempUpdateFile == null) {
|
||||
downloadStatus = "failed";
|
||||
return;
|
||||
}
|
||||
|
||||
// 下载完成后不自动重启,等待用户确认
|
||||
downloadStatus = "completed";
|
||||
System.out.println("下载完成,等待用户确认安装...");
|
||||
|
||||
// 将下载文件路径保存,供后续安装使用
|
||||
this.tempUpdateFilePath = tempUpdateFile;
|
||||
}
|
||||
|
||||
private String tempUpdateFilePath = null;
|
||||
|
||||
|
||||
/**
|
||||
* 下载更新文件
|
||||
*/
|
||||
private String downloadUpdate(String downloadUrl) {
|
||||
try {
|
||||
downloadStatus = "downloading";
|
||||
downloadProgress = 0;
|
||||
downloadedBytes = 0;
|
||||
downloadStartTime = System.currentTimeMillis();
|
||||
|
||||
URL url = new URL(downloadUrl);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||
connection.setConnectTimeout(10000);
|
||||
connection.setReadTimeout(30000);
|
||||
|
||||
// 获取文件大小
|
||||
totalBytes = connection.getContentLength();
|
||||
if (totalBytes <= 0) {
|
||||
totalBytes = 70 * 1024 * 1024; // 默认70MB
|
||||
}
|
||||
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
String updateFileName = "erp_update_" + System.currentTimeMillis() + ".exe";
|
||||
String tempUpdatePath = Paths.get(tempDir, updateFileName).toString();
|
||||
|
||||
System.out.println("开始下载更新文件,大小: " + (totalBytes / 1024 / 1024) + "MB");
|
||||
|
||||
try (InputStream inputStream = connection.getInputStream();
|
||||
FileOutputStream outputStream = new FileOutputStream(tempUpdatePath)) {
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
long lastSpeedUpdate = System.currentTimeMillis();
|
||||
long lastDownloadedBytes = 0;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
if (downloadCancelled) {
|
||||
System.out.println("下载已取消,停止下载进程");
|
||||
downloadStatus = "cancelled";
|
||||
downloadSpeed = "已取消";
|
||||
outputStream.close();
|
||||
try {
|
||||
java.io.File partialFile = new java.io.File(tempUpdatePath);
|
||||
if (partialFile.exists()) {
|
||||
partialFile.delete();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("删除部分下载文件失败: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
downloadedBytes += bytesRead;
|
||||
|
||||
// 计算下载进度
|
||||
downloadProgress = (int) ((downloadedBytes * 100) / totalBytes);
|
||||
// 每秒计算一次下载速度
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - lastSpeedUpdate >= 1000) {
|
||||
long speedBytes = downloadedBytes - lastDownloadedBytes;
|
||||
double speedKB = speedBytes / 1024.0;
|
||||
|
||||
if (speedKB >= 1024) {
|
||||
downloadSpeed = String.format("%.1f MB/s", speedKB / 1024.0);
|
||||
} else {
|
||||
downloadSpeed = String.format("%.1f KB/s", speedKB);
|
||||
}
|
||||
|
||||
lastSpeedUpdate = currentTime;
|
||||
lastDownloadedBytes = downloadedBytes;
|
||||
|
||||
System.out.println(String.format("下载进度: %d%% (%d/%d MB) 速度: %s",
|
||||
downloadProgress,
|
||||
downloadedBytes / 1024 / 1024,
|
||||
totalBytes / 1024 / 1024,
|
||||
downloadSpeed));
|
||||
}
|
||||
|
||||
// 防止阻塞UI线程
|
||||
if (downloadedBytes % (1024 * 1024) == 0) { // 每MB休息一下
|
||||
Thread.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
downloadStatus = "completed";
|
||||
downloadProgress = 100;
|
||||
downloadSpeed = "完成";
|
||||
System.out.println("下载完成: " + (downloadedBytes / 1024 / 1024) + "MB");
|
||||
}
|
||||
|
||||
return tempUpdatePath;
|
||||
} catch (Exception e) {
|
||||
downloadStatus = "failed";
|
||||
downloadSpeed = "失败";
|
||||
System.err.println("下载失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建更新脚本
|
||||
*/
|
||||
private String createUpdateScript(String currentExePath, String tempUpdateFile) {
|
||||
try {
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
String scriptPath = Paths.get(tempDir, "update_erp.bat").toString();
|
||||
|
||||
File currentDir = new File(System.getProperty("user.dir"));
|
||||
File targetExe = new File(currentDir, "erpClient.exe");
|
||||
String targetPath = targetExe.getAbsolutePath();
|
||||
|
||||
StringBuilder script = new StringBuilder();
|
||||
script.append("@echo off\r\n");
|
||||
script.append("chcp 65001 > nul\r\n");
|
||||
script.append("echo 正在等待程序完全退出...\r\n");
|
||||
script.append("timeout /t 1 /nobreak > nul\r\n");
|
||||
script.append("echo 开始更新程序文件...\r\n");
|
||||
script.append("if exist \"").append(targetPath).append("\" (");
|
||||
script.append(" move \"").append(targetPath).append("\" \"").append(targetPath).append(".backup\"");
|
||||
script.append(" )\r\n");
|
||||
script.append("move \"").append(tempUpdateFile).append("\" \"").append(targetPath).append("\"\r\n");
|
||||
script.append("if exist \"").append(targetPath).append("\" (\r\n");
|
||||
script.append(" echo 更新成功!程序将在1秒后重新启动...\r\n");
|
||||
script.append(" timeout /t 1 /nobreak > nul\r\n");
|
||||
script.append(" start \"\" \"").append(targetPath).append("\"\r\n");
|
||||
script.append(" if exist \"").append(targetPath).append(".backup\" del \"").append(targetPath).append(".backup\"\r\n");
|
||||
script.append(") else (\r\n");
|
||||
script.append(" echo 更新失败!正在恢复原版本...\r\n");
|
||||
script.append(" if exist \"").append(targetPath).append(".backup\" (\r\n");
|
||||
script.append(" move \"").append(targetPath).append(".backup\" \"").append(targetPath).append("\"\r\n");
|
||||
script.append(" echo 已恢复原版本,程序将在1秒后重新启动...\r\n");
|
||||
script.append(" timeout /t 1 /nobreak > nul\r\n");
|
||||
script.append(" start \"\" \"").append(targetPath).append("\"\r\n");
|
||||
script.append(" )\r\n");
|
||||
script.append(")\r\n");
|
||||
script.append("echo 更新操作完成!\r\n");
|
||||
script.append("timeout /t 1 /nobreak > nul\r\n");
|
||||
script.append("(goto) 2>nul & del \"%~f0\" & exit\r\n");
|
||||
|
||||
Files.write(Paths.get(scriptPath), script.toString().getBytes("GBK"));
|
||||
return scriptPath;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新脚本并退出程序
|
||||
*/
|
||||
private void executeUpdateAndExit(String scriptPath) {
|
||||
try {
|
||||
System.out.println("下载完成!正在准备更新...");
|
||||
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", scriptPath);
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
|
||||
pb.redirectError(ProcessBuilder.Redirect.DISCARD);
|
||||
pb.start();
|
||||
System.out.println("更新程序已启动,当前程序即将退出...");
|
||||
Thread.sleep(1000);
|
||||
Runtime.getRuntime().halt(0);
|
||||
} catch (Exception e) {
|
||||
Runtime.getRuntime().halt(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存跳过的版本
|
||||
*/
|
||||
@PostMapping("/skip-version")
|
||||
public Map<String, Object> saveSkippedVersion(@RequestBody Map<String, String> request) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
saveUpdateInfo("skippedUpdateVersion", request.get("version"));
|
||||
result.put("success", true);
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 1688产品实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "alibaba_1688_products")
|
||||
@Data
|
||||
public class Alibaba1688ProductEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_id", unique = true, nullable = false)
|
||||
private String productId;
|
||||
|
||||
@Column(name = "title", length = 1000)
|
||||
private String title;
|
||||
|
||||
@Column(name = "price")
|
||||
private String price;
|
||||
|
||||
@Column(name = "image_url", length = 1000)
|
||||
private String imageUrl;
|
||||
|
||||
@Column(name = "product_url", length = 1000)
|
||||
private String productUrl;
|
||||
|
||||
@Column(name = "supplier")
|
||||
private String supplier;
|
||||
|
||||
@Column(name = "category")
|
||||
private String category;
|
||||
|
||||
@Column(name = "min_order")
|
||||
private String minOrder;
|
||||
|
||||
@Column(name = "trade_assurance")
|
||||
private String tradeAssurance;
|
||||
|
||||
@Column(name = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Amazon产品缓存实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "amazon_products")
|
||||
@Data
|
||||
public class AmazonProductEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@Column(unique = true, nullable = false)
|
||||
private String asin;
|
||||
|
||||
@Column(name = "title", length = 1000)
|
||||
private String title;
|
||||
|
||||
@Column(name = "price")
|
||||
private String price;
|
||||
|
||||
@Column(name = "image_url", length = 1000)
|
||||
private String imageUrl;
|
||||
|
||||
@Column(name = "product_url", length = 1000)
|
||||
private String productUrl;
|
||||
|
||||
@Column(name = "brand")
|
||||
private String brand;
|
||||
|
||||
@Column(name = "category")
|
||||
private String category;
|
||||
|
||||
@Column(name = "rating")
|
||||
private String rating;
|
||||
|
||||
@Column(name = "review_count")
|
||||
private String reviewCount;
|
||||
|
||||
@Column(name = "availability")
|
||||
private String availability;
|
||||
|
||||
@Column(name = "seller")
|
||||
private String seller;
|
||||
|
||||
@Column(name = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 认证令牌实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "auth_tokens")
|
||||
@Data
|
||||
public class AuthTokenEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "service_name", unique = true, nullable = false)
|
||||
private String serviceName;
|
||||
|
||||
@Column(name = "token", nullable = false)
|
||||
private String token;
|
||||
|
||||
@Column(name = "expire_time")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 斑马订单实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "banma_orders")
|
||||
@Data
|
||||
public class BanmaOrderEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "tracking_number", unique = true, nullable = false)
|
||||
private String trackingNumber;
|
||||
|
||||
@Column(name = "order_data", columnDefinition = "TEXT")
|
||||
private String orderData;
|
||||
|
||||
@Column(name = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 缓存数据实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "cache_data")
|
||||
@Data
|
||||
public class CacheDataEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "cache_key", unique = true, nullable = false)
|
||||
private String cacheKey;
|
||||
|
||||
@Column(name = "cache_value", columnDefinition = "TEXT")
|
||||
private String cacheValue;
|
||||
|
||||
@Column(name = "expire_time")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonRawValue;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 乐天产品缓存实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "rakuten_products")
|
||||
@Data
|
||||
public class RakutenProductEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "original_shop_name")
|
||||
private String originalShopName;
|
||||
|
||||
@Column(name = "shop_name")
|
||||
private String shopName;
|
||||
|
||||
@Column(name = "product_url", length = 1000)
|
||||
private String productUrl;
|
||||
|
||||
@Column(name = "img_url", length = 1000)
|
||||
private String imgUrl;
|
||||
|
||||
@Column(name = "product_title", length = 500)
|
||||
private String productTitle;
|
||||
|
||||
@Column(name = "product_name", length = 500)
|
||||
private String productName;
|
||||
|
||||
@Column(name = "price")
|
||||
private String price;
|
||||
|
||||
@Column(name = "ranking")
|
||||
private String ranking;
|
||||
|
||||
@Column(name = "price_1688")
|
||||
private String price1688;
|
||||
|
||||
|
||||
@Column(name = "detail_url_1688", length = 1000)
|
||||
private String detailUrl1688;
|
||||
|
||||
@Column(name = "image_1688_url", length = 1000)
|
||||
private String image1688Url;
|
||||
|
||||
|
||||
@Column(name = "map_recognition_link", length = 1000)
|
||||
private String mapRecognitionLink; // 1688识图链接
|
||||
|
||||
@Column(name = "freight")
|
||||
private Double freight; // 运费
|
||||
|
||||
@Column(name = "median")
|
||||
private Double median; // 中位价格
|
||||
|
||||
@Column(name = "weight")
|
||||
private String weight; // 重量
|
||||
@Column(name = "sku_price_json", columnDefinition = "JSON")
|
||||
private String skuPriceJson; // SKU价格JSON字符串
|
||||
|
||||
@Column(name = "session_id")
|
||||
private String sessionId; // 用于标识一次导入会话
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 更新状态实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "update_status")
|
||||
public class UpdateStatusEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "key_name", unique = true)
|
||||
private String keyName;
|
||||
|
||||
@Column(name = "value_data", columnDefinition = "TEXT")
|
||||
private String valueData;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getKeyName() {
|
||||
return keyName;
|
||||
}
|
||||
|
||||
public void setKeyName(String keyName) {
|
||||
this.keyName = keyName;
|
||||
}
|
||||
|
||||
public String getValueData() {
|
||||
return valueData;
|
||||
}
|
||||
|
||||
public void setValueData(String valueData) {
|
||||
this.valueData = valueData;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 斑马订单缓存实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "zebra_orders")
|
||||
@Data
|
||||
public class ZebraOrderEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "order_id")
|
||||
private String orderId;
|
||||
|
||||
@Column(name = "product_name", length = 500)
|
||||
private String productName;
|
||||
|
||||
@Column(name = "quantity")
|
||||
private Integer quantity;
|
||||
|
||||
@Column(name = "price_jpy")
|
||||
private String priceJpy;
|
||||
|
||||
@Column(name = "price_cny")
|
||||
private String priceCny;
|
||||
|
||||
@Column(name = "image_url", length = 1000)
|
||||
private String imageUrl;
|
||||
|
||||
@Column(name = "customer_name")
|
||||
private String customerName;
|
||||
|
||||
@Column(name = "order_status")
|
||||
private String orderStatus;
|
||||
|
||||
@Column(name = "order_date")
|
||||
private String orderDate;
|
||||
|
||||
@Column(name = "shipping_address", length = 1000)
|
||||
private String shippingAddress;
|
||||
|
||||
@Column(name = "tracking_number")
|
||||
private String trackingNumber;
|
||||
|
||||
@Column(name = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.tashow.erp.fx.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
public class JavaBridge {
|
||||
|
||||
/**
|
||||
* 直接保存字节数组为Excel文件到桌面(纯 Spring Boot 环境,无文件对话框)
|
||||
*/
|
||||
public String saveExcelFileToDesktop(byte[] data, String fileName) {
|
||||
try {
|
||||
if (data == null || data.length == 0) {
|
||||
log.warn("文件数据为空,无法保存文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
String userHome = System.getProperty("user.home");
|
||||
File desktop = new File(userHome, "Desktop");
|
||||
if (!desktop.exists()) {
|
||||
// 回退到用户目录
|
||||
desktop = new File(userHome);
|
||||
}
|
||||
|
||||
File file = new File(desktop, fileName);
|
||||
int counter = 1;
|
||||
if (fileName != null && fileName.contains(".")) {
|
||||
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
String extension = fileName.substring(fileName.lastIndexOf('.'));
|
||||
while (file.exists()) {
|
||||
file = new File(desktop, baseName + "_" + counter + extension);
|
||||
counter++;
|
||||
}
|
||||
} else {
|
||||
while (file.exists()) {
|
||||
file = new File(desktop, fileName + "_" + counter);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||
fos.write(data);
|
||||
fos.flush();
|
||||
}
|
||||
|
||||
String filePath = file.getAbsolutePath();
|
||||
log.info("Excel文件已保存: {}", filePath);
|
||||
return filePath;
|
||||
} catch (IOException e) {
|
||||
log.error("保存Excel文件失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到系统剪贴板
|
||||
*/
|
||||
public boolean copyToClipboard(String text) {
|
||||
try {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
StringSelection selection = new StringSelection(text);
|
||||
clipboard.setContents(selection, null);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("复制到剪贴板失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// package com.tashow.erp.fx.controller;
|
||||
//
|
||||
// 已转为纯 Spring Boot,不再包含 JavaFX 控制器与视图逻辑。如需恢复 FX,请取消注释并恢复依赖。
|
||||
//
|
||||
// import javafx.application.Platform;
|
||||
// import javafx.fxml.FXML;
|
||||
// import javafx.fxml.Initializable;
|
||||
// import javafx.scene.layout.BorderPane;
|
||||
// import javafx.scene.web.WebEngine;
|
||||
// import javafx.scene.web.WebView;
|
||||
// import lombok.extern.slf4j.Slf4j;
|
||||
// import net.rgielen.fxweaver.core.FxmlView;
|
||||
// import org.springframework.stereotype.Component;
|
||||
// import netscape.javascript.JSObject;
|
||||
//
|
||||
// import java.net.URL;
|
||||
// import java.util.ResourceBundle;
|
||||
//
|
||||
// @Slf4j
|
||||
// @Component
|
||||
// @FxmlView("/static/fxml/Main.fxml")
|
||||
// public class MainCtrl implements Initializable {
|
||||
// @FXML
|
||||
// public BorderPane rootPane;
|
||||
// @FXML
|
||||
// public WebView webView;
|
||||
// private WebEngine webEngine;
|
||||
// @Override
|
||||
// public void initialize(URL location, ResourceBundle resources) {
|
||||
// if (Platform.isFxApplicationThread()) {
|
||||
// initWebView();
|
||||
// } else {
|
||||
// Platform.runLater(this::initWebView);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void initWebView() {
|
||||
// webEngine = webView.getEngine();
|
||||
// webEngine.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...");
|
||||
// webEngine.setJavaScriptEnabled(true);
|
||||
// webEngine.load("http://localhost:8081/html/erp-dashboard.html");
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,11 @@
|
||||
// package com.tashow.erp.fx.view;
|
||||
//
|
||||
// 已转为纯 Spring Boot,移除 FX 视图。
|
||||
// import javafx.scene.Parent;
|
||||
// import net.rgielen.fxweaver.core.FxmlView;
|
||||
// import org.springframework.stereotype.Component;
|
||||
//
|
||||
// @Component
|
||||
// @FxmlView("/static/fxml/Main.fxml")
|
||||
// public class MainView {
|
||||
// }
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.tashow.erp.model;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 1688爬取风控监控数据表实体类
|
||||
*/
|
||||
@Data
|
||||
public class ClientAlibaba1688Monitor {
|
||||
private Long id;
|
||||
|
||||
private String clientId;
|
||||
|
||||
private String ipAddress;
|
||||
|
||||
private String eventType;
|
||||
|
||||
private Long eventTime;
|
||||
|
||||
private Long duration;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.tashow.erp.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 监控数据类
|
||||
*/
|
||||
@Data
|
||||
public class MonitoringData {
|
||||
private String ipAddress;
|
||||
private MonitorEventType eventType;
|
||||
private long eventTime;
|
||||
private long duration;
|
||||
|
||||
/**
|
||||
* 风控事件类型
|
||||
*/
|
||||
public enum MonitorEventType {
|
||||
MOBILE_FIRST_ACCESS, // 移动端首次访问
|
||||
MOBILE_BLOCKED, // 移动端被风控
|
||||
DESKTOP_BLOCKED // 电脑端被风控
|
||||
}
|
||||
}
|
||||