Compare commits
54 Commits
e643ab0713
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 358203b11d | |||
| 02858146b3 | |||
| bff057c99b | |||
| d29d4d69da | |||
| 937a84bb81 | |||
| f9d1848280 | |||
| dd23d9fe90 | |||
| 007799fb2a | |||
| cfb9096788 | |||
| cce281497b | |||
| 92ab782943 | |||
| c2e1617a99 | |||
| 7c7009ffed | |||
| 2f00fde3be | |||
| cfb70d5830 | |||
| 4e2ce48934 | |||
| a62d7b6147 | |||
| c9874f1786 | |||
| 87a4a2fed0 | |||
| d0a930d4f2 | |||
| 6443cdc8d0 | |||
| 1aceceb38f | |||
| 84087ddf80 | |||
| 7e065c1a0b | |||
| 0be60bc103 | |||
| 35c9fc205a | |||
| 3a76aaa3c0 | |||
| e2a438c84e | |||
| 5468dc53fc | |||
| 17b6a7b9f9 | |||
| 901d67d2dc | |||
| 1be22664c4 | |||
| 281ae6a846 | |||
| 17f03c3ade | |||
| 0c85aa5677 | |||
| d9f91b77e3 | |||
| 07e34c35c8 | |||
| 6e1b4d00de | |||
| 132299c4b7 | |||
| 6f04658265 | |||
| f614860eee | |||
| 4c2546733e | |||
| a0d94b2c70 | |||
| d77ebab153 | |||
| a28d846638 | |||
| 1c4dd2dd6d | |||
| 6f22c9bffd | |||
| 4fbe51d625 | |||
| db67a99288 | |||
| 4065da3766 | |||
| 61235c5610 | |||
| 8d16d0b286 | |||
| 52ce0e1969 | |||
| e650a7c7f3 |
48
.claude/1.md
@@ -1,48 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Your rule content
|
||||
#角色
|
||||
你是一名精通开发的高级工程师,拥有10年以上的应用开发经验,熟悉*等开发工具和技术栈。
|
||||
你的任务是帮助用户设计和开发易用且易于推护的 *** 应用。始终遵循最佳实践,并坚持干净代码和健壮架构的原则。
|
||||
|
||||
#目标
|
||||
你的目标是以用户容易理解的方式帮助他们完成“应用的设计和开发工作,确保应用功能完善、性能优异、用户体验良好。
|
||||
|
||||
#要求
|
||||
在理解用户需求、设计UI、编写代码、解决问题和项目选代优化时,你应该始终遵循以下原则:
|
||||
|
||||
|
||||
##需求理解
|
||||
-充分理解用户需求,站在用户角度思考,分析需求是否存在缺漏,并与用户讨论完善需求;
|
||||
-选择最简单的解决方案来满足用户需求,避免过度设计。
|
||||
##UI和样式设计
|
||||
-使用现代UI框架进行样式设计(例如***,这里可以根据不同开发项目仔纽展开,比如使用哪些视觉规范或者UI框架,没有的话也可以不用过多展开);
|
||||
-在不同平台上实现一致的设计和响应式模式
|
||||
##代码编写
|
||||
技术选型:根据项目需求选择合适的技术栈(例如***,这里需要仔细展开,比如介招某个技术栈用在什么地方,以及要遵循什么最佳实践)
|
||||
代码结构:强调代码的清晰性、模块化、可维护性,遵循最佳实践(如DRY原则、最小权限原则、响应式设计等)
|
||||
-代码安全性:在编写代码时,始终考虑安全性,避免引入漏洞,确保用户输入的安全处理
|
||||
-性能优化:优化代码的性能,减少资源占用,提升加载速度,确保项目的高效运行
|
||||
-测试与文档:编写单元测试,确保代码的健壮性,并提供清晰的中文注释和文档。方便后续阅读和维护
|
||||
##问题解决
|
||||
-全面阅读相关代码,理解***应用的工作原理
|
||||
-根据用户的反馈分析问题的原因,提出解决问题的思路
|
||||
-确保每次代码变更不会破坏现有功能,且尽可能保持最小的改动
|
||||
##迭代优化
|
||||
与用户保持密切沟通,根据反读调整功能和设计,确保应用符合用户需求
|
||||
在不确定需求时,主动询问用户以澄清需求或技术细节
|
||||
##方法论
|
||||
-系统2思维:以分析严谨的方式解决问题。将需求分解为更小、可管理的部分,并在实施前仔细考虑每一步
|
||||
思维树:评估多种可能的解决方案及其后果。使用结构化的方法探索不同的路径。并选择最优的解决方案
|
||||
-选代改进:在最终确定代码之前,考虑改进、边缘情况和优化。通过潜在增强的迭代,确保最终解决方案是健壮的
|
||||
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
|
||||
---
|
||||
|
||||
# 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中的规范,代码简洁度和性能优先。
|
||||
💡 **操作提示**:在每次修改代码前,先向我说明修改的思路和方案,我确认同意后再进行代码更改。
|
||||
|
||||
---
|
||||
@@ -1,222 +0,0 @@
|
||||
{
|
||||
"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:*)",
|
||||
"WebFetch(domain:www.electronjs.org)",
|
||||
"Bash(test:*)",
|
||||
"Bash(sqlite3:*)",
|
||||
"Bash(npx electron:*)",
|
||||
"Bash(.erpClient.exe)",
|
||||
"Bash(start erpClient.exe)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(npx asar list:*)",
|
||||
"Bash(npx @electron/asar extract:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": [],
|
||||
"additionalDirectories": [
|
||||
"C:\\c\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue",
|
||||
"C:\\Users\\ZiJIe\\Desktop\\wox",
|
||||
"C:\\c\\Users\\ZiJIe"
|
||||
]
|
||||
},
|
||||
"dangerouslySkipPermissions": true
|
||||
}
|
||||
72
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
*.temp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
nul
|
||||
|
||||
# IDE 文件
|
||||
## IntelliJ IDEA
|
||||
@@ -14,6 +15,25 @@ Thumbs.db
|
||||
*.ipr
|
||||
out/
|
||||
.idea_modules/
|
||||
|
||||
## VSCode
|
||||
.vscode/
|
||||
.history/
|
||||
|
||||
## Eclipse
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
## AI 助手配置
|
||||
.claude/
|
||||
.cursor/
|
||||
.starFactory/
|
||||
.windsurf/
|
||||
.aider/
|
||||
.copilot/
|
||||
|
||||
# Maven 构建目录
|
||||
/ruoyi-common/target/
|
||||
/ruoyi-system/target/
|
||||
/ruoyi-quartz/target/
|
||||
@@ -21,4 +41,54 @@ out/
|
||||
/ruoyi-framework/target/
|
||||
/ruoyi-admin/target/
|
||||
/erp_client_sb/target/
|
||||
target /
|
||||
target/
|
||||
*.class
|
||||
|
||||
# Node.js 前端项目
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.npm
|
||||
.yarn/
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.local
|
||||
|
||||
# Vite 构建缓存
|
||||
.vite/
|
||||
.vite-inspect/
|
||||
|
||||
# Electron 构建产物
|
||||
build/
|
||||
release/
|
||||
out/
|
||||
*.exe
|
||||
*.dmg
|
||||
*.AppImage
|
||||
|
||||
# Java 打包文件
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# JRE 运行时环境
|
||||
jre/
|
||||
jdk/
|
||||
|
||||
# 数据库文件
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 缓存和临时文件
|
||||
.cache/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
25
.idea/compiler.xml
generated
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<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
@@ -1,20 +0,0 @@
|
||||
<?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>
|
||||
40
.idea/jarRepositories.xml
generated
@@ -1,40 +0,0 @@
|
||||
<?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="http://maven.aliyun.com/nexus/content/groups/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
@@ -1,13 +0,0 @@
|
||||
<?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_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
7
.idea/vcs.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?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>
|
||||
465
.idea/workspace.xml
generated
@@ -1,465 +0,0 @@
|
||||
<?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">
|
||||
<change afterPath="$PROJECT_DIR$/electron-vue-template/public/erp_client_sb-2.4.7.jar" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.claude/settings.local.json" beforeDir="false" afterPath="$PROJECT_DIR$/.claude/settings.local.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/device.id" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/erp-cache.db" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/erp-cache.db-shm" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/erp-cache.db-wal" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/jwt_rsa_private.pem" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/jwt_rsa_public.pem" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/electron-builder.json" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/electron-builder.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/main/main.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/main/main.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/main/preload.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/main/preload.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/App.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/App.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/http.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/http.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/zebra.ts" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/api/zebra.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/AccountManager.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/AccountManager.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/UpdateDialog.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/common/UpdateDialog.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/layout/NavigationBar.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/layout/NavigationBar.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/rakuten/RakutenDashboard.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/electron-vue-template/src/renderer/components/zebra/ZebraDashboard.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/erp_client_sb/data/device.id" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/erp_client_sb/data/erp-cache.db" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/erp_client_sb/src/main/java/com/tashow/erp/service/impl/AmazonScrapingServiceImpl.java" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java" beforeDir="false" afterPath="$PROJECT_DIR$/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java" afterDir="false" />
|
||||
</list>
|
||||
<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="MavenImportPreferences">
|
||||
<option name="generalSettings">
|
||||
<MavenGeneralSettings>
|
||||
<option name="userSettingsFile" value="C:\Program Files\apache-maven-3.9.4\conf\settings.xml" />
|
||||
</MavenGeneralSettings>
|
||||
</option>
|
||||
</component>
|
||||
<component name="MavenRunner">
|
||||
<option name="skipTests" value="true" />
|
||||
</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": {
|
||||
"Maven.erp_client_sb [clean].executor": "Run",
|
||||
"Maven.erp_client_sb [install].executor": "Run",
|
||||
"Maven.erp_client_sb [verify].executor": "Run",
|
||||
"Maven.ruoyi [clean].executor": "Run",
|
||||
"Maven.ruoyi [install].executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RequestMappingsPanelOrder0": "0",
|
||||
"RequestMappingsPanelOrder1": "1",
|
||||
"RequestMappingsPanelWidth0": "75",
|
||||
"RequestMappingsPanelWidth1": "75",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Spring Boot.ErpClientSbApplication.executor": "Debug",
|
||||
"Spring Boot.RuoYiApplication.executor": "Debug",
|
||||
"Spring Boot.未命名.executor": "Debug",
|
||||
"git-widget-placeholder": "master",
|
||||
"last_opened_file_path": "C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/electron-vue-template/public",
|
||||
"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",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.build:win.executor": "Run",
|
||||
"npm.run.executor": "Run",
|
||||
"settings.editor.selected.configurable": "com.intellij.platform.ide.impl.presentationAssistant.PresentationAssistantConfigurable",
|
||||
"ts.external.directory.path": "C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\electron-vue-template\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="ReactorSettings">
|
||||
<option name="notificationShown" value="true" />
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\electron-vue-template\public" />
|
||||
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\electron-vue-template\dist\win-unpacked" />
|
||||
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="C:\Users\ZiJIe\Desktop\wox\RuoYi-Vue\electron-vue-template\dist\win-unpacked" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunDashboard">
|
||||
<option name="configurationTypes">
|
||||
<set>
|
||||
<option value="SpringBootApplicationConfigurationType" />
|
||||
<option value="js.build_tools.npm" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
<component name="RunManager" selected="npm.build:win">
|
||||
<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">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="17" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="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">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="17" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="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>
|
||||
<configuration name="build" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/electron-vue-template/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="build:win" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/electron-vue-template/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="build:win" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="run" type="js.build_tools.npm" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/electron-vue-template/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue="npm.run" />
|
||||
<item itemvalue="npm.build" />
|
||||
<item itemvalue="npm.build:win" />
|
||||
<item itemvalue="Spring Boot.RuoYiApplication" />
|
||||
<item itemvalue="Spring Boot.ErpClientSbApplication" />
|
||||
</list>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="npm.build:win" />
|
||||
<item itemvalue="npm.build" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</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" />
|
||||
<workItem from="1758694285287" duration="16954000" />
|
||||
<workItem from="1758765346934" duration="13417000" />
|
||||
<workItem from="1758780949720" duration="2793000" />
|
||||
<workItem from="1758783772919" duration="3262000" />
|
||||
<workItem from="1758787264618" duration="6950000" />
|
||||
<workItem from="1758794319987" duration="13284000" />
|
||||
<workItem from="1758860156056" duration="54529000" />
|
||||
<workItem from="1759112919536" duration="1025000" />
|
||||
<workItem from="1759114000963" duration="12514000" />
|
||||
<workItem from="1759130476790" duration="1021000" />
|
||||
<workItem from="1759131666876" duration="1987000" />
|
||||
<workItem from="1759133675854" duration="9993000" />
|
||||
</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>
|
||||
<task id="LOCAL-00003" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758521046022</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758521046022</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758522202417</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758522202417</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758522758523</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758522758523</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758523822682</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758523822682</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758524938236</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758524938236</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758525299299</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758525299299</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758525500986</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758525500986</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758526085800</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758526085800</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758528696003</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758528696003</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758529627894</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758529627894</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758619260259</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758619260259</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758619552979</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758619552979</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00015" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758683139068</created>
|
||||
<option name="number" value="00015" />
|
||||
<option name="presentableId" value="LOCAL-00015" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758683139068</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00016" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758683285305</created>
|
||||
<option name="number" value="00016" />
|
||||
<option name="presentableId" value="LOCAL-00016" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758683285305</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00017" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758683484212</created>
|
||||
<option name="number" value="00017" />
|
||||
<option name="presentableId" value="LOCAL-00017" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758683484212</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00018" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758787441900</created>
|
||||
<option name="number" value="00018" />
|
||||
<option name="presentableId" value="LOCAL-00018" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758787441900</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00019" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758787531138</created>
|
||||
<option name="number" value="00019" />
|
||||
<option name="presentableId" value="LOCAL-00019" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758787531138</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00020" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758787581342</created>
|
||||
<option name="number" value="00020" />
|
||||
<option name="presentableId" value="LOCAL-00020" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758787581342</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00021" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758787954763</created>
|
||||
<option name="number" value="00021" />
|
||||
<option name="presentableId" value="LOCAL-00021" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758787954763</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00022" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758788061529</created>
|
||||
<option name="number" value="00022" />
|
||||
<option name="presentableId" value="LOCAL-00022" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758788061529</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00023" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758795230077</created>
|
||||
<option name="number" value="00023" />
|
||||
<option name="presentableId" value="LOCAL-00023" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758795230077</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00024" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758875248722</created>
|
||||
<option name="number" value="00024" />
|
||||
<option name="presentableId" value="LOCAL-00024" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758875248722</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758877545022</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758877545022</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758953146637</created>
|
||||
<option name="number" value="00026" />
|
||||
<option name="presentableId" value="LOCAL-00026" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758953146637</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00027" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1758966134103</created>
|
||||
<option name="number" value="00027" />
|
||||
<option name="presentableId" value="LOCAL-00027" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1758966134103</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00028" summary="1">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759129266815</created>
|
||||
<option name="number" value="00028" />
|
||||
<option name="presentableId" value="LOCAL-00028" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759129266815</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="29" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="1" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="1" />
|
||||
</component>
|
||||
</project>
|
||||
403
CLAUDE.md
@@ -1,211 +1,270 @@
|
||||
|
||||
---
|
||||
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
## Project Overview
|
||||
|
||||
这是一个基于 **RuoYi-Vue 3.9.0** 的企业管理系统,集成了跨境电商 ERP 功能。项目包含:
|
||||
This is a **hybrid ERP system** consisting of:
|
||||
1. **Backend**: RuoYi-Vue (Spring Boot 2.5.15 + MyBatis) - Java 17 management system
|
||||
2. **Desktop Client**: Electron + Vue 3 + TypeScript application
|
||||
3. **Embedded Spring Boot Service**: Java service that runs within the Electron app
|
||||
|
||||
* **RuoYi-Vue 核心**:基于 Spring Boot 2.5.15 后端和 Vue.js 2.6.12 前端的企业管理平台
|
||||
* **ERP 客户端 (erp_client_sb)**:独立的跨境电商 ERP 桌面应用 (JavaFX + Spring Boot 3.5.4)
|
||||
* **客户端监控扩展**:实时监控、数据报表、API 集成等自定义功能
|
||||
The architecture uses a dual-service pattern where the Electron app communicates with both:
|
||||
- Local embedded Spring Boot service (port 8081)
|
||||
- Remote RuoYi admin backend (port 8085)
|
||||
|
||||
## 项目架构
|
||||
|
||||
### 主项目模块结构 (Maven 多模块)
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
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 客户端
|
||||
C:\wox\erp\
|
||||
├── electron-vue-template/ # Electron + Vue 3 desktop client
|
||||
│ ├── src/
|
||||
│ │ ├── main/ # Electron main process (TypeScript)
|
||||
│ │ └── renderer/ # Vue 3 renderer process
|
||||
│ │ ├── api/ # API client modules
|
||||
│ │ ├── components/ # Vue components
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── scripts/ # Build scripts
|
||||
│ └── package.json
|
||||
├── ruoyi-admin/ # Main Spring Boot application entry
|
||||
├── ruoyi-system/ # System management module
|
||||
├── ruoyi-framework/ # Framework core (Security, Redis, etc.)
|
||||
├── ruoyi-common/ # Common utilities
|
||||
├── ruoyi-generator/ # Code generator
|
||||
├── ruoyi-quartz/ # Scheduled tasks
|
||||
├── erp_client_sb/ # Embedded Spring Boot service for client
|
||||
├── sql/ # Database migration scripts
|
||||
└── pom.xml # Root Maven configuration
|
||||
```
|
||||
|
||||
### 技术栈详情
|
||||
## Development Commands
|
||||
|
||||
**后端 (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)
|
||||
### Backend (Spring Boot)
|
||||
|
||||
```bash
|
||||
# 启动主应用 (推荐)
|
||||
cd ruoyi-admin && mvn spring-boot:run
|
||||
# Build the project (from root)
|
||||
mvn clean package
|
||||
|
||||
# 从根目录启动
|
||||
mvn spring-boot:run -pl ruoyi-admin
|
||||
# Run the RuoYi admin backend
|
||||
cd ruoyi-admin
|
||||
mvn spring-boot:run
|
||||
# Runs on http://localhost:8085
|
||||
|
||||
# 打包部署
|
||||
# Build without tests
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 编译项目
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
### 前端开发 (npm)
|
||||
### Frontend (Electron + Vue)
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
cd electron-vue-template
|
||||
|
||||
# 安装依赖 (建议使用国内镜像)
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# 启动开发服务器 (端口 80)
|
||||
# Development mode with hot reload
|
||||
npm run dev
|
||||
|
||||
# 生产环境构建
|
||||
npm run build:prod
|
||||
|
||||
# 测试环境构建
|
||||
npm run build:stage
|
||||
# Build for distribution
|
||||
npm run build # Cross-platform
|
||||
npm run build:win # Windows
|
||||
npm run build:mac # macOS
|
||||
npm run build:linux # Linux
|
||||
```
|
||||
|
||||
### ERP 客户端开发
|
||||
## Key Architecture Patterns
|
||||
|
||||
```bash
|
||||
cd erp_client_sb
|
||||
### 1. Dual-Backend Routing (http.ts)
|
||||
|
||||
# 启动 JavaFX 应用
|
||||
mvn spring-boot:run
|
||||
# 或者
|
||||
mvn javafx:run
|
||||
The Electron client uses intelligent routing to determine which backend to call:
|
||||
- Paths starting with `/monitor/`, `/system/`, `/tool/banma`, `/tool/genmai` → RuoYi backend (port 8085)
|
||||
- All other paths → Embedded Spring Boot service (port 8081)
|
||||
|
||||
# 打包可执行 JAR
|
||||
mvn clean package
|
||||
**Location**: `electron-vue-template/src/renderer/api/http.ts`
|
||||
|
||||
### 2. Account-Based Resource Isolation
|
||||
|
||||
User-specific resources (splash images, brand logos) are stored per account:
|
||||
- Backend stores URLs in the `client_account` table (columns: `splash_image`, `brand_logo`)
|
||||
- Files are uploaded to Qiniu Cloud (七牛云) configured in `application.yml`
|
||||
- Each user sees only their own uploaded assets
|
||||
|
||||
**Key files**:
|
||||
- Java entity: `ruoyi-system/src/main/java/com/ruoyi/system/domain/ClientAccount.java`
|
||||
- MyBatis mapper: `ruoyi-system/src/main/resources/mapper/system/ClientAccountMapper.xml`
|
||||
- API endpoints: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ClientAccountController.java`
|
||||
|
||||
### 3. Event-Driven UI Updates
|
||||
|
||||
The Vue application uses custom browser events to propagate state changes between components:
|
||||
- `window.dispatchEvent(new CustomEvent('brandLogoChanged'))` - notifies when brand logo changes
|
||||
- `window.addEventListener('brandLogoChanged', handler)` - listens for changes in App.vue
|
||||
|
||||
This pattern ensures immediate UI updates after upload/delete operations without requiring page refreshes.
|
||||
|
||||
### 4. VIP Feature Gating
|
||||
|
||||
Certain features (e.g., custom splash images, brand logos) are gated by VIP status:
|
||||
- Check `accountType` field in `ClientAccount` (values: `trial`, `paid`)
|
||||
- Trial accounts show `TrialExpiredDialog` when attempting VIP features
|
||||
- VIP validation happens in `SettingsDialog.vue` before allowing uploads
|
||||
|
||||
## Important Configuration
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
**File**: `ruoyi-admin/src/main/resources/application.yml`
|
||||
|
||||
Key settings:
|
||||
- Server port: `8085`
|
||||
- Upload path: `ruoyi.profile: D:/ruoyi/uploadPath`
|
||||
- Redis: `8.138.23.49:6379` (password: `123123`)
|
||||
- Qiniu Cloud credentials for file storage
|
||||
- Token expiration: 30 minutes
|
||||
|
||||
### Database
|
||||
|
||||
The system uses MySQL with MyBatis. When adding new fields:
|
||||
1. Write SQL migration script in `sql/` directory
|
||||
2. Update Java entity in `ruoyi-system/src/main/java/com/ruoyi/system/domain/`
|
||||
3. Update MyBatis mapper XML in `ruoyi-system/src/main/resources/mapper/system/`
|
||||
4. Include field in `<resultMap>`, `<sql id="select...">`, `<insert>`, and `<update>` sections
|
||||
|
||||
### Electron Main Process
|
||||
|
||||
**File**: `electron-vue-template/src/main/main.ts`
|
||||
|
||||
- Manages embedded Spring Boot process lifecycle
|
||||
- Handles splash screen display
|
||||
- Configures tray icon
|
||||
- Manages auto-updates
|
||||
- Uses app data directory: `app.getPath('userData')`
|
||||
|
||||
## Development Workflow (from .cursor/rules/guize.mdc)
|
||||
|
||||
When making code changes, follow this three-phase approach:
|
||||
|
||||
### Phase 1: Analyze Problem (【分析问题】)
|
||||
- Understand user intent and ask clarifying questions
|
||||
- Search all related code
|
||||
- Identify root cause
|
||||
- Look for code smells: duplication, poor naming, outdated patterns, inconsistent types
|
||||
- Ask questions if multiple solutions exist
|
||||
|
||||
### Phase 2: Plan Solution (【制定方案】)
|
||||
- List files to be created/modified/deleted
|
||||
- Describe changes briefly for each file
|
||||
- Eliminate code duplication through reuse/abstraction
|
||||
- Ensure DRY principles and good architecture
|
||||
- Ask questions if key decisions are unclear
|
||||
|
||||
### Phase 3: Execute (【执行方案】)
|
||||
- Implement according to the approved plan
|
||||
- Run type checking after modifications
|
||||
- **DO NOT** commit code unless explicitly requested
|
||||
- **DO NOT** start dev servers automatically
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Adding a New API Endpoint
|
||||
|
||||
1. **Backend** (Spring Boot):
|
||||
```java
|
||||
// In appropriate Controller (e.g., ClientAccountController.java)
|
||||
@PostMapping("/your-endpoint")
|
||||
public AjaxResult yourMethod(@RequestBody YourDTO dto) {
|
||||
// Implementation
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Frontend** (Vue/TypeScript):
|
||||
```typescript
|
||||
// In electron-vue-template/src/renderer/api/your-module.ts
|
||||
export const yourApi = {
|
||||
async yourMethod(data: YourType) {
|
||||
return http.post<ResponseType>('/your-endpoint', data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Component usage**:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { yourApi } from '@/api/your-module'
|
||||
|
||||
const handleAction = async () => {
|
||||
try {
|
||||
const res = await yourApi.yourMethod(data)
|
||||
// Handle success, update local state immediately
|
||||
localState.value = res.data
|
||||
// Dispatch event if other components need to know
|
||||
window.dispatchEvent(new CustomEvent('yourEventName'))
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### File Upload Pattern
|
||||
|
||||
```typescript
|
||||
// Frontend
|
||||
const handleUpload = async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('username', currentUsername)
|
||||
|
||||
const res = await splashApi.uploadSomething(file, username)
|
||||
if (res.url) {
|
||||
localImageUrl.value = res.url // Update immediately
|
||||
window.dispatchEvent(new CustomEvent('imageChanged'))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 核心配置与架构要点
|
||||
```java
|
||||
// Backend Controller
|
||||
@PostMapping("/upload")
|
||||
public AjaxResult upload(@RequestParam("file") MultipartFile file) {
|
||||
String url = qiniuService.uploadFile(file);
|
||||
// Save URL to database
|
||||
return AjaxResult.success(url);
|
||||
}
|
||||
```
|
||||
|
||||
### Maven 依赖管理
|
||||
## Technology Stack Details
|
||||
|
||||
- 使用阿里云 Maven 镜像 (maven.aliyun.com)
|
||||
- 父 POM 统一管理版本号和依赖
|
||||
- 安全版本覆盖:Tomcat 9.0.106, Logback 1.2.13, Spring Framework 5.3.39
|
||||
### Backend
|
||||
- **Framework**: Spring Boot 2.5.15
|
||||
- **Security**: Spring Security 5.7.12 + JWT
|
||||
- **ORM**: MyBatis with PageHelper
|
||||
- **Database**: MySQL
|
||||
- **Cache**: Redis (Lettuce client)
|
||||
- **File Storage**: Qiniu Cloud (七牛云)
|
||||
- **API Docs**: Swagger 3.0.0
|
||||
- **Build**: Maven
|
||||
|
||||
### 前端开发配置
|
||||
### Frontend
|
||||
- **Framework**: Vue 3.3.8 (Composition API with `<script setup>`)
|
||||
- **Desktop**: Electron 32.1.2
|
||||
- **Build**: Vite 4.5.0
|
||||
- **UI Library**: Element Plus 2.11.3
|
||||
- **Language**: TypeScript 5.2.2
|
||||
- **Excel**: ExcelJS 4.4.0
|
||||
|
||||
- **开发服务器**:端口 80,支持热重载
|
||||
- **代理配置**:`vue.config.js` 中配置后端 API 代理 (`http://8.138.23.49:8080`)
|
||||
- **构建优化**:Gzip 压缩、代码分割、Element UI 单独打包
|
||||
## Testing
|
||||
|
||||
### 数据库与缓存
|
||||
Currently, there is no explicit test framework configured. When adding tests:
|
||||
- Backend: Use JUnit with Spring Boot Test
|
||||
- Frontend: Consider Vitest (already compatible with Vite)
|
||||
|
||||
- **MySQL**:主数据库,通过 Druid 连接池管理
|
||||
- **Redis**:会话存储、缓存、分布式锁
|
||||
- **SQLite**:ERP 客户端本地数据存储
|
||||
## Important Notes
|
||||
|
||||
### 安全与认证
|
||||
|
||||
- **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
|
||||
---
|
||||
|
||||
⚠️ **额外要求**:回答时必须使用中文。
|
||||
💡 **操作提示**:在每次修改代码前,我会先向您说明修改的思路和方案,请您确认同意后再进行代码更改。
|
||||
|
||||
|
||||
---
|
||||
- **Chinese Language**: All user-facing text should be in Chinese (simplified)
|
||||
- **Code Style**: Follow existing patterns - keep code concise and avoid unnecessary abstractions
|
||||
- **No Auto-commit**: Never commit changes unless explicitly requested by the user
|
||||
- **Secrets**: Qiniu Cloud keys are in `application.yml` - never expose in client code
|
||||
- **Token Management**: JWT tokens stored in Electron via `utils/token.ts`, sent in `Authorization` header
|
||||
- **Image Proxy**: Custom protocol handler in Electron for loading images from backend
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,77 +0,0 @@
|
||||
<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);
|
||||
```
|
||||
@@ -9,15 +9,13 @@
|
||||
"public/jre/**/*",
|
||||
"public/icon/**/*",
|
||||
"public/image/**/*",
|
||||
"public/splash.html"
|
||||
"public/splash.html",
|
||||
"public/config/**/*"
|
||||
],
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "http://192.168.1.89:8080/static/updates/"
|
||||
},
|
||||
"electronLanguages": ["zh-CN", "en-US"],
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
@@ -25,13 +23,11 @@
|
||||
"shortcutName": "erpClient"
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "public/icon/icon.png"
|
||||
},
|
||||
"linux": {
|
||||
"target": ["snap"]
|
||||
"target": "dir",
|
||||
"icon": "public/icon/icon1.png"
|
||||
},
|
||||
"files": [
|
||||
"package.json",
|
||||
{
|
||||
"from": "build/main",
|
||||
"to": "main",
|
||||
@@ -40,19 +36,14 @@
|
||||
{
|
||||
"from": "build/renderer",
|
||||
"to": "renderer",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
|
||||
"from": "src/main/static",
|
||||
"to": "static",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "public",
|
||||
"to": "assets",
|
||||
"filter": [
|
||||
"erp_client_sb-*.jar"
|
||||
"**/*",
|
||||
"!icon/**/*",
|
||||
"!image/**/*",
|
||||
"!jre/**/*",
|
||||
"!config/**/*",
|
||||
"!*.jar",
|
||||
"!splash.html"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -63,58 +54,32 @@
|
||||
"icon/**/*",
|
||||
"image/**/*",
|
||||
"splash.html",
|
||||
"config/**/*",
|
||||
"!erp_client_sb-*.jar",
|
||||
"!data/**/*",
|
||||
"!jre/bin/jabswitch.exe",
|
||||
"!jre/bin/jaccessinspector.exe",
|
||||
"!jre/bin/jaccesswalker.exe",
|
||||
"!jre/bin/jar.exe",
|
||||
"!jre/bin/jarsigner.exe",
|
||||
"!jre/bin/javac.exe",
|
||||
"!jre/bin/javadoc.exe",
|
||||
"!jre/bin/javap.exe",
|
||||
"!jre/bin/jcmd.exe",
|
||||
"!jre/bin/jconsole.exe",
|
||||
"!jre/bin/jdb.exe",
|
||||
"!jre/bin/jdeprscan.exe",
|
||||
"!jre/bin/jdeps.exe",
|
||||
"!jre/bin/jfr.exe",
|
||||
"!jre/bin/jhsdb.exe",
|
||||
"!jre/bin/jimage.exe",
|
||||
"!jre/bin/jinfo.exe",
|
||||
"!jre/bin/jlink.exe",
|
||||
"!jre/bin/jmap.exe",
|
||||
"!jre/bin/jmod.exe",
|
||||
"!jre/bin/jpackage.exe",
|
||||
"!jre/bin/jps.exe",
|
||||
"!jre/bin/jrunscript.exe",
|
||||
"!jre/bin/jshell.exe",
|
||||
"!jre/bin/jstack.exe",
|
||||
"!jre/bin/jstat.exe",
|
||||
"!jre/bin/jstatd.exe",
|
||||
"!jre/bin/keytool.exe",
|
||||
"!jre/bin/kinit.exe",
|
||||
"!jre/bin/klist.exe",
|
||||
"!jre/bin/ktab.exe",
|
||||
"!jre/bin/rmiregistry.exe",
|
||||
"!jre/bin/serialver.exe",
|
||||
"!jre/bin/*.exe",
|
||||
"jre/bin/java.exe",
|
||||
"jre/bin/javaw.exe",
|
||||
"jre/bin/keytool.exe",
|
||||
"!jre/include/**",
|
||||
"!jre/lib/src.zip",
|
||||
"!jre/lib/ct.sym",
|
||||
"!jre/lib/jvm.lib",
|
||||
"!icon/image.png",
|
||||
"!icon/img.png"
|
||||
"!jre/lib/jvm.lib"
|
||||
]
|
||||
},
|
||||
"!build",
|
||||
"!dist",
|
||||
"!scripts"
|
||||
],
|
||||
"electronLanguages": ["en", "zh-CN"],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "update-helper.bat",
|
||||
"to": "../update-helper.bat"
|
||||
},
|
||||
{
|
||||
"from": "public",
|
||||
"to": "./",
|
||||
"filter": ["erp_client_sb-*.jar"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1806
electron-vue-template/package-lock.json
generated
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "electron-vue-template",
|
||||
"name": "erpClient",
|
||||
"version": "0.1.0",
|
||||
"description": "A minimal Electron + Vue application",
|
||||
"main": "main/main.js",
|
||||
"main": "build/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"
|
||||
"build": "node scripts/build.js && electron-builder --dir",
|
||||
"build:win": "node scripts/build.js && electron-builder --win --dir",
|
||||
"build:mac": "node scripts/build.js && electron-builder --mac --dir",
|
||||
"build:linux": "node scripts/build.js && electron-builder --linux --dir"
|
||||
},
|
||||
"repository": "https://github.com/deluze/electron-vue-template",
|
||||
"author": {
|
||||
@@ -17,10 +17,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.4.1",
|
||||
"binary-extensions": "^3.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^38.1.2",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^25.1.6",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"express": "^5.1.0",
|
||||
"fs-extra": "^11.3.2",
|
||||
"typescript": "^5.2.2",
|
||||
@@ -31,5 +33,26 @@
|
||||
"element-plus": "^2.11.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"vue": "^3.3.8"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.tashow.erp",
|
||||
"productName": "天骄智能电商",
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"node_modules/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "dist"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "dir",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4417
electron-vue-template/pnpm-lock.yaml
generated
Normal file
52
electron-vue-template/public/config/logback.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 使用 Spring Boot 传递的日志路径 -->
|
||||
<property name="LOG_HOME" value="${LOG_PATH:-logs}" />
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出 - 按天滚动 -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/spring-boot.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/spring-boot-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>1GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 设置根日志级别 - 同时输出到控制台和文件 -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
|
||||
<!-- 设置特定包的日志级别 -->
|
||||
<logger name="com.tashow.erp" level="INFO" additivity="false">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</logger>
|
||||
|
||||
<!-- 确保 Hibernate 日志也输出 -->
|
||||
<logger name="org.hibernate" level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</logger>
|
||||
|
||||
<!-- 确保 Spring 日志也输出 -->
|
||||
<logger name="org.springframework" level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</logger>
|
||||
</configuration>
|
||||
|
||||
BIN
electron-vue-template/public/icon/acquisition.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
electron-vue-template/public/icon/amazon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
electron-vue-template/public/icon/anjldk.png
Normal file
|
After Width: | Height: | Size: 638 B |
BIN
electron-vue-template/public/icon/asin.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
electron-vue-template/public/icon/done.png
Normal file
|
After Width: | Height: | Size: 731 B |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 4.6 KiB |
BIN
electron-vue-template/public/icon/icon1.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
electron-vue-template/public/icon/inProgress.png
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
electron-vue-template/public/icon/networkErrors.png
Normal file
|
After Width: | Height: | Size: 628 B |
BIN
electron-vue-template/public/icon/plsb.png
Normal file
|
After Width: | Height: | Size: 894 B |
BIN
electron-vue-template/public/icon/rakuten.png
Normal file
|
After Width: | Height: | Size: 804 B |
BIN
electron-vue-template/public/icon/vipExclusive.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
electron-vue-template/public/icon/waiting.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
electron-vue-template/public/icon/wlymx.png
Normal file
|
After Width: | Height: | Size: 384 B |
BIN
electron-vue-template/public/icon/zebra.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
electron-vue-template/public/image/excel-format-example.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
electron-vue-template/public/image/img.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
electron-vue-template/public/image/img_1.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
electron-vue-template/public/image/user.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,31 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<html>
|
||||
<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; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
.image {
|
||||
flex: 1;
|
||||
background-image: __SPLASH_IMAGE__;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.box {
|
||||
height: 64px;
|
||||
padding: 0 30px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: rgba(0,0,0,0.85);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.progress {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(0,0,0,0.06);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, #1677ff 0%, #4096ff 100%);
|
||||
border-radius: 10px;
|
||||
animation: load 3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
.btn {
|
||||
padding: 4px 15px;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: rgba(0,0,0,0.65);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
@keyframes load {
|
||||
to { width: 90%; }
|
||||
}
|
||||
.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="image"></div>
|
||||
<div class="box">
|
||||
<span class="text">正在启动</span>
|
||||
<div class="progress"><div class="bar"></div></div>
|
||||
<button class="btn" onclick="require('electron').ipcRenderer.send('quit-app')">退出</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
28
electron-vue-template/scripts/copy-assets.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const Path = require('path');
|
||||
const FileSystem = require('fs-extra');
|
||||
|
||||
async function copyAssets() {
|
||||
console.log('Copying static assets from public directory...');
|
||||
|
||||
// 注释:icon 和 image 资源已统一由 public 目录管理
|
||||
// electron-builder 会直接从 public 打包这些资源到 app.asar.unpacked
|
||||
// 不需要复制到 build/renderer,避免重复打包导致体积增大
|
||||
|
||||
// const publicDir = Path.join(__dirname, '..', 'public');
|
||||
// const buildRendererDir = Path.join(__dirname, '..', 'build', 'renderer');
|
||||
|
||||
// await FileSystem.copy(
|
||||
// Path.join(publicDir, 'icon'),
|
||||
// Path.join(buildRendererDir, 'icon'),
|
||||
// { overwrite: true }
|
||||
// );
|
||||
// await FileSystem.copy(
|
||||
// Path.join(publicDir, 'image'),
|
||||
// Path.join(buildRendererDir, 'image'),
|
||||
// { overwrite: true }
|
||||
// );
|
||||
|
||||
console.log('Static assets copy skipped (resources managed by public directory).');
|
||||
}
|
||||
|
||||
module.exports = copyAssets;
|
||||
@@ -25,7 +25,7 @@ async function startRenderer() {
|
||||
}
|
||||
|
||||
async function startElectron() {
|
||||
if (electronProcess) { // single instance lock
|
||||
if (electronProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async function startElectron() {
|
||||
process.stdout.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()))
|
||||
});
|
||||
|
||||
electronProcess.stderr.on('data', data =>
|
||||
electronProcess.stderr.on('data', data =>
|
||||
process.stderr.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()))
|
||||
);
|
||||
|
||||
|
||||
@@ -3,14 +3,18 @@ const Chalk = require('chalk');
|
||||
|
||||
function compile(directory) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tscProcess = ChildProcess.exec('tsc', {
|
||||
const tscProcess = ChildProcess.exec('npx tsc', {
|
||||
cwd: directory,
|
||||
});
|
||||
|
||||
tscProcess.stdout.on('data', data =>
|
||||
tscProcess.stdout.on('data', data =>
|
||||
process.stdout.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString()))
|
||||
);
|
||||
|
||||
tscProcess.stderr.on('data', data =>
|
||||
process.stderr.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString()))
|
||||
);
|
||||
|
||||
tscProcess.on('exit', exitCode => {
|
||||
if (exitCode > 0) {
|
||||
reject(exitCode);
|
||||
|
||||
@@ -3,11 +3,15 @@ import { contextBridge, ipcRenderer } from 'electron'
|
||||
const electronAPI = {
|
||||
sendMessage: (message: string) => ipcRenderer.send('message', message),
|
||||
|
||||
getJarVersion: () => ipcRenderer.invoke('get-jar-version'),
|
||||
|
||||
downloadUpdate: (downloadUrl: string) => ipcRenderer.invoke('download-update', downloadUrl),
|
||||
getDownloadProgress: () => ipcRenderer.invoke('get-download-progress'),
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
checkPendingUpdate: () => ipcRenderer.invoke('check-pending-update'),
|
||||
clearUpdateFiles: () => ipcRenderer.invoke('clear-update-files'),
|
||||
|
||||
// 添加文件保存对话框 API
|
||||
showSaveDialog: (options: any) => ipcRenderer.invoke('show-save-dialog', options),
|
||||
@@ -15,8 +19,40 @@ const electronAPI = {
|
||||
showOpenDialog: (options: any) => ipcRenderer.invoke('show-open-dialog', options),
|
||||
// 添加文件写入 API
|
||||
writeFile: (filePath: string, data: Uint8Array) => ipcRenderer.invoke('write-file', filePath, data),
|
||||
// 添加日志相关 API
|
||||
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
||||
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
||||
// 关闭行为配置 API
|
||||
getCloseAction: () => ipcRenderer.invoke('get-close-action'),
|
||||
setCloseAction: (action: 'quit' | 'minimize' | 'tray') => ipcRenderer.invoke('set-close-action', action),
|
||||
|
||||
// 缓存管理 API
|
||||
clearCache: () => ipcRenderer.invoke('clear-cache'),
|
||||
|
||||
// 启动配置 API
|
||||
getLaunchConfig: () => ipcRenderer.invoke('get-launch-config'),
|
||||
setLaunchConfig: (config: { autoLaunch: boolean; launchMinimized: boolean }) => ipcRenderer.invoke('set-launch-config', config),
|
||||
|
||||
// 刷新页面 API
|
||||
reload: () => ipcRenderer.invoke('reload'),
|
||||
|
||||
// 窗口控制 API
|
||||
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
|
||||
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||
|
||||
// 开屏图片相关 API
|
||||
saveSplashConfig: (username: string, imageUrl: string) => ipcRenderer.invoke('save-splash-config', username, imageUrl),
|
||||
getSplashConfig: () => ipcRenderer.invoke('get-splash-config'),
|
||||
|
||||
// 品牌logo相关 API
|
||||
saveBrandLogoConfig: (username: string, logoUrl: string) => ipcRenderer.invoke('save-brand-logo-config', username, logoUrl),
|
||||
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||
clearUserConfig: () => ipcRenderer.invoke('clear-user-config'),
|
||||
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.removeAllListeners('download-progress')
|
||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||
},
|
||||
removeDownloadProgressListener: () => {
|
||||
|
||||
71
electron-vue-template/src/main/tray.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { app, Tray, Menu, BrowserWindow, nativeImage } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
|
||||
let tray: Tray | null = null
|
||||
|
||||
function getIconPath(): string {
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
if (isDev) {
|
||||
return join(__dirname, '../../public/icon/icon1.png')
|
||||
}
|
||||
return join(process.resourcesPath, 'app.asar.unpacked', 'public/icon/icon1.png')
|
||||
}
|
||||
|
||||
export function createTray(mainWindow: BrowserWindow | null) {
|
||||
if (tray) return tray
|
||||
|
||||
const iconPath = getIconPath()
|
||||
const icon = nativeImage.createFromPath(iconPath)
|
||||
tray = new Tray(icon.resize({ width: 16, height: 16 }))
|
||||
|
||||
tray.setToolTip('ERP客户端 - 后台运行中')
|
||||
|
||||
// 左键点击显示窗口
|
||||
tray.on('click', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
// 右键菜单
|
||||
updateTrayMenu(mainWindow)
|
||||
return tray
|
||||
}
|
||||
|
||||
export function updateTrayMenu(mainWindow: BrowserWindow | null) {
|
||||
if (!tray) return
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示窗口',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '退出应用',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
export function destroyTray() {
|
||||
if (tray) {
|
||||
tray.destroy()
|
||||
tray = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
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 });
|
||||
getProductsBatch(asinList: string[], batchId: string, region: string, signal?: AbortSignal) {
|
||||
return http.post<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/batch', { asinList, batchId, region }, signal);
|
||||
},
|
||||
|
||||
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 });
|
||||
},
|
||||
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');
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,113 +1,32 @@
|
||||
import { http } from './http';
|
||||
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;
|
||||
export interface LoginParams {
|
||||
username: string
|
||||
password: string
|
||||
clientId?: string
|
||||
}
|
||||
|
||||
// 认证相关类型定义
|
||||
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;
|
||||
export interface AuthResponse {
|
||||
token: string
|
||||
permissions?: string
|
||||
accountName?: string
|
||||
expireTime?: string
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
// 用户登录
|
||||
login(params: LoginRequest) {
|
||||
return http
|
||||
.post('/api/login', params)
|
||||
.then(res => unwrap<LoginResponse>(res));
|
||||
login(params: LoginParams) {
|
||||
return http.post<{ data: AuthResponse }>('/monitor/account/login', params)
|
||||
},
|
||||
|
||||
// 用户注册
|
||||
register(params: RegisterRequest) {
|
||||
return http
|
||||
.post('/api/register', params)
|
||||
.then(res => unwrap<RegisterResponse>(res));
|
||||
register(params: { username: string; password: string; deviceId?: string }) {
|
||||
return http.post<{ data: AuthResponse }>('/monitor/account/register', params)
|
||||
},
|
||||
|
||||
// 检查用户名可用性
|
||||
checkUsername(username: string) {
|
||||
return http
|
||||
.get('/api/check-username', { username })
|
||||
.then(res => {
|
||||
if (res && res.code === 200) {
|
||||
return { available: res.data };
|
||||
}
|
||||
throw new Error(res?.msg || '检查用户名失败');
|
||||
});
|
||||
return http.get<{ data: boolean }>('/monitor/account/check-username', { username })
|
||||
},
|
||||
|
||||
// 验证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 });
|
||||
},
|
||||
|
||||
// 删除token缓存
|
||||
deleteTokenCache() {
|
||||
return http.postVoid('/api/cache/delete?key=token');
|
||||
},
|
||||
// 保存token到本地数据库
|
||||
saveToken(token: string) {
|
||||
return http.postVoid('/api/cache/save', { key: 'token', value: token });
|
||||
},
|
||||
|
||||
// 从本地数据库获取token
|
||||
getToken(): Promise<string | undefined> {
|
||||
return http.get<any>('/api/cache/get?key=token').then((res: any) => {
|
||||
if (typeof res === 'string') return res;
|
||||
if (res && typeof res === 'object') {
|
||||
if (typeof res.code === 'number') {
|
||||
return res.code === 0 ? (res.data as string | undefined) : undefined;
|
||||
}
|
||||
if (typeof (res as any).data === 'string') return (res as any).data as string;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
},
|
||||
|
||||
// 会话引导:检查并恢复会话(返回体各异,这里保持 any)
|
||||
sessionBootstrap() {
|
||||
return http.get<any>('/api/session/bootstrap');
|
||||
},
|
||||
};
|
||||
return http.post<{ data: AuthResponse }>('/monitor/account/verify', { token })
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,45 @@
|
||||
import { http } from './http'
|
||||
|
||||
// 与老版保持相同的接口路径与参数
|
||||
const base = '/api/device'
|
||||
export interface DeviceItem {
|
||||
deviceId: string
|
||||
name?: string
|
||||
os?: string
|
||||
status: 'online' | 'offline'
|
||||
lastActiveAt?: string
|
||||
isCurrent?: boolean
|
||||
}
|
||||
|
||||
export interface DeviceQuota {
|
||||
limit: number
|
||||
used: number
|
||||
}
|
||||
|
||||
export interface DeviceItem {
|
||||
deviceId: string
|
||||
name?: string
|
||||
status?: 'online' | 'offline'
|
||||
lastActiveAt?: string
|
||||
}
|
||||
|
||||
// 统一处理AjaxResult格式
|
||||
function handleAjaxResult(res: any) {
|
||||
if (res?.code !== 200) {
|
||||
throw new Error(res?.msg || '操作失败')
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
export const deviceApi = {
|
||||
getQuota(username: string): Promise<DeviceQuota> {
|
||||
return http.get(`${base}/quota`, { username }).then(handleAjaxResult)
|
||||
getQuota(username: string) {
|
||||
return http.get<{ data: DeviceQuota }>('/monitor/device/quota', { username })
|
||||
},
|
||||
|
||||
list(username: string): Promise<DeviceItem[]> {
|
||||
return http.get(`${base}/list`, { username }).then(handleAjaxResult)
|
||||
list(username: string) {
|
||||
return http.get<{ data: DeviceItem[] }>('/monitor/device/list', { username })
|
||||
},
|
||||
|
||||
register(payload: { username: string }) {
|
||||
return http.post(`${base}/register`, payload).then(handleAjaxResult)
|
||||
async register(payload: { username: string; deviceId: string; os?: string }) {
|
||||
const [ipRes, nameRes] = await Promise.all([
|
||||
http.get<{ data: string }>('/api/system/local-ip'),
|
||||
http.get<{ data: string }>('/api/system/computer-name')
|
||||
])
|
||||
return http.post('/monitor/device/register', {
|
||||
...payload,
|
||||
ip: ipRes.data,
|
||||
computerName: nameRes.data
|
||||
})
|
||||
},
|
||||
|
||||
remove(payload: { deviceId: string }) {
|
||||
return http.post(`${base}/remove`, payload).then(handleAjaxResult)
|
||||
},
|
||||
|
||||
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
|
||||
return http.post(`${base}/heartbeat`, payload).then(handleAjaxResult)
|
||||
remove(payload: { deviceId: string; username: string }) {
|
||||
return http.post('/monitor/device/remove', payload)
|
||||
},
|
||||
|
||||
offline(payload: { deviceId: string }) {
|
||||
return http.post(`${base}/offline`, payload).then(handleAjaxResult)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
return http.post('/monitor/device/offline', payload)
|
||||
}
|
||||
}
|
||||
21
electron-vue-template/src/renderer/api/feedback.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface FeedbackParams {
|
||||
username: string
|
||||
deviceId: string
|
||||
feedbackContent: string
|
||||
logDate?: string
|
||||
logFile?: File
|
||||
}
|
||||
|
||||
export const feedbackApi = {
|
||||
submit(data: FeedbackParams) {
|
||||
const formData = new FormData()
|
||||
formData.append('username', data.username)
|
||||
formData.append('deviceId', data.deviceId)
|
||||
formData.append('feedbackContent', data.feedbackContent)
|
||||
if (data.logDate) formData.append('logDate', data.logDate)
|
||||
if (data.logFile) formData.append('logFile', data.logFile)
|
||||
return http.upload('/monitor/feedback/submit', formData)
|
||||
}
|
||||
}
|
||||
39
electron-vue-template/src/renderer/api/genmai.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface GenmaiAccount {
|
||||
id?: number
|
||||
name?: string
|
||||
username: string
|
||||
password: string
|
||||
clientUsername?: string
|
||||
token?: string
|
||||
tokenExpireAt?: string
|
||||
status?: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export const genmaiApi = {
|
||||
getAccounts(name?: string) {
|
||||
return http.get('/tool/genmai/accounts', name ? { name } : undefined)
|
||||
},
|
||||
|
||||
getAccountLimit(name?: string) {
|
||||
return http.get('/tool/genmai/account-limit', name ? { name } : undefined)
|
||||
},
|
||||
|
||||
saveAccount(body: GenmaiAccount, name?: string) {
|
||||
const url = name ? `/tool/genmai/accounts?name=${encodeURIComponent(name)}` : '/tool/genmai/accounts'
|
||||
return http.post(url, body)
|
||||
},
|
||||
|
||||
removeAccount(id: number) {
|
||||
return http.delete(`/tool/genmai/accounts/${id}`)
|
||||
},
|
||||
|
||||
validateAndRefresh(id: number) {
|
||||
return http.post(`/tool/genmai/accounts/${id}/validate`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,139 @@
|
||||
// 极简 HTTP 工具:封装 GET/POST,按路径选择后端服务
|
||||
export type HttpMethod = 'GET' | 'POST';
|
||||
import { AppConfig, isRuoyiPath } from '../config'
|
||||
|
||||
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
|
||||
const BASE_RUOYI = 'http://192.168.1.89:8080';
|
||||
export type HttpMethod = 'GET' | 'POST' | 'DELETE'
|
||||
export const CONFIG = AppConfig
|
||||
|
||||
function resolveBase(path: string): string {
|
||||
// 走 ruoyi-admin 的路径:鉴权与版本、平台工具路由
|
||||
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
|
||||
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
|
||||
// 其他默认走客户端服务
|
||||
return BASE_CLIENT;
|
||||
return isRuoyiPath(path) ? CONFIG.RUOYI_BASE : CONFIG.CLIENT_BASE
|
||||
}
|
||||
|
||||
// 将对象转为查询字符串
|
||||
function buildQuery(params?: Record<string, unknown>): string {
|
||||
if (!params) return '';
|
||||
const usp = new URLSearchParams();
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null) return;
|
||||
usp.append(key, String(value));
|
||||
if (value != null) query.append(key, String(value));
|
||||
});
|
||||
const queryString = usp.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
return query.toString() ? `?${query}` : '';
|
||||
}
|
||||
|
||||
// 统一请求入口:自动加上 BASE_URL、JSON 头与错误处理
|
||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${resolveBase(path)}${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}`);
|
||||
async function getToken(): Promise<string> {
|
||||
try {
|
||||
const tokenModule = await import('../utils/token');
|
||||
return tokenModule.getToken() || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsername(): Promise<string> {
|
||||
try {
|
||||
const tokenModule = await import('../utils/token');
|
||||
return tokenModule.getUsernameFromToken() || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit & { signal?: AbortSignal }): Promise<T> {
|
||||
const token = await getToken();
|
||||
const username = await getUsername();
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch(`${resolveBase(path)}${path}`, {
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...(username ? { 'username': username } : {}),
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error('无法连接服务器,请检查网络后重试');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status >= 500) {
|
||||
throw new Error('无法连接服务器,请检查网络后重试');
|
||||
}
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(text || '无法连接服务器,请检查网络后重试');
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
return (await res.json()) as T;
|
||||
const json: any = await res.json();
|
||||
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||
const error: any = new Error(json.msg || '请求失败');
|
||||
error.code = json.code;
|
||||
throw error;
|
||||
}
|
||||
return 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' });
|
||||
get<T>(path: string, params?: Record<string, unknown>, signal?: AbortSignal) {
|
||||
return request<T>(`${path}${buildQuery(params)}`, { method: 'GET', signal });
|
||||
},
|
||||
post<T>(path: string, body?: unknown) {
|
||||
return request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined });
|
||||
post<T>(path: string, body?: unknown, signal?: AbortSignal) {
|
||||
return request<T>(path, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal
|
||||
});
|
||||
},
|
||||
|
||||
delete<T>(path: string) {
|
||||
return request<T>(path, { method: 'DELETE' });
|
||||
},
|
||||
// 用于无需读取响应体的 POST(如删除/心跳等),从根源避免读取中断
|
||||
postVoid(path: string, body?: unknown) {
|
||||
return fetch(`${resolveBase(path)}${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(`${resolveBase(path)}${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}`);
|
||||
|
||||
async upload<T>(path: string, form: FormData, signal?: AbortSignal) {
|
||||
const token = await getToken();
|
||||
const username = await getUsername();
|
||||
let res: Response;
|
||||
|
||||
try {
|
||||
res = await fetch(`${resolveBase(path)}${path}`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...(username ? { 'username': username } : {})
|
||||
},
|
||||
signal
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error('无法连接服务器,请检查网络后重试');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status >= 500) {
|
||||
throw new Error('无法连接服务器,请检查网络后重试');
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
});
|
||||
},
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(text || '无法连接服务器,请检查网络后重试');
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
const json: any = await res.json();
|
||||
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||
const error: any = new Error(json.msg || '请求失败');
|
||||
error.code = json.code;
|
||||
throw error;
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
return (await res.text()) as unknown as T;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
115
electron-vue-template/src/renderer/api/mark.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { http } from './http'
|
||||
|
||||
export const markApi = {
|
||||
// 新建任务(调用 erp_client_sb)
|
||||
newTask(file: File, signal?: AbortSignal) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return http.upload<{ code: number, data: any, msg: string }>('/api/trademark/newTask', formData, signal)
|
||||
},
|
||||
|
||||
// 获取任务列表及筛选数据(调用 erp_client_sb)
|
||||
getTask(signal?: AbortSignal) {
|
||||
return http.post<{
|
||||
code: number,
|
||||
data: {
|
||||
original: any,
|
||||
filtered: Record<string, any>[], // 完整的行数据(Map格式)
|
||||
headers: string[] // 表头
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/task', undefined, signal)
|
||||
},
|
||||
|
||||
// 品牌商标筛查
|
||||
brandCheck(brands: string[], taskId?: string, signal?: AbortSignal) {
|
||||
return http.post<{ code: number, data: { total: number, checked: number, registered: number, unregistered: number, failed: number, data: any[], duration: string }, msg: string }>('/api/trademark/brandCheck', { brands, taskId }, signal)
|
||||
},
|
||||
|
||||
// 查询品牌筛查进度
|
||||
getBrandCheckProgress(taskId: string) {
|
||||
return http.get<{ code: number, data: { current: number }, msg: string }>('/api/trademark/brandCheckProgress', { taskId })
|
||||
},
|
||||
|
||||
// 取消品牌筛查任务
|
||||
cancelBrandCheck(taskId: string) {
|
||||
return http.post<{ code: number, data: string, msg: string }>('/api/trademark/cancelBrandCheck', { taskId })
|
||||
},
|
||||
|
||||
// 验证Excel表头
|
||||
validateHeaders(file: File, requiredHeaders?: string[]) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (requiredHeaders && requiredHeaders.length > 0) {
|
||||
formData.append('requiredHeaders', JSON.stringify(requiredHeaders))
|
||||
}
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
headers: string[],
|
||||
valid?: boolean,
|
||||
missing?: string[]
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/validateHeaders', formData)
|
||||
},
|
||||
|
||||
// 从Excel提取品牌列表(客户端本地接口,返回完整Excel数据)
|
||||
extractBrands(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
total: number,
|
||||
brands: string[],
|
||||
headers: string[],
|
||||
allRows: Record<string, any>[]
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/extractBrands', formData)
|
||||
},
|
||||
|
||||
// 根据ASIN列表从Excel中过滤完整行数据(客户端本地接口)
|
||||
filterByAsins(file: File, asins: string[]) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('asins', JSON.stringify(asins))
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
headers: string[],
|
||||
filteredRows: Record<string, any>[],
|
||||
total: number
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/filterByAsins', formData)
|
||||
},
|
||||
|
||||
// 根据品牌列表从Excel中过滤完整行数据(客户端本地接口)
|
||||
filterByBrands(file: File, brands: string[]) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('brands', JSON.stringify(brands))
|
||||
return http.upload<{
|
||||
code: number,
|
||||
data: {
|
||||
headers: string[],
|
||||
filteredRows: Record<string, any>[],
|
||||
total: number
|
||||
},
|
||||
msg: string
|
||||
}>('/api/trademark/filterByBrands', formData)
|
||||
},
|
||||
|
||||
// 保存查询会话
|
||||
saveSession(sessionData: any) {
|
||||
return http.post<{ code: number, data: { sessionId: string }, msg: string }>('/api/trademark/saveSession', sessionData)
|
||||
},
|
||||
|
||||
// 恢复查询会话
|
||||
getSession(sessionId: string) {
|
||||
return http.get<{ code: number, data: any, msg: string }>('/api/trademark/getSession', { sessionId })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
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;
|
||||
}
|
||||
import { http } from './http'
|
||||
|
||||
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));
|
||||
getProducts(params: { file?: File; shopName?: string; batchId?: string }, signal?: AbortSignal) {
|
||||
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, signal)
|
||||
},
|
||||
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));
|
||||
|
||||
search1688(imageUrl: string, sessionId?: string, signal?: AbortSignal) {
|
||||
const payload: Record<string, unknown> = { imageUrl }
|
||||
if (sessionId) payload.sessionId = sessionId
|
||||
return http.post('/api/rakuten/search1688', payload, signal)
|
||||
},
|
||||
|
||||
getLatestProducts() {
|
||||
return http.get('/api/rakuten/products/latest').then(res => unwrap<{ products: any[] }>(res));
|
||||
},
|
||||
};
|
||||
return http.get('/api/rakuten/products/latest')
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
55
electron-vue-template/src/renderer/api/splash.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface SplashImageResponse {
|
||||
splashImage: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const splashApi = {
|
||||
// 上传开屏图片
|
||||
async uploadSplashImage(file: File, username: string) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('username', username)
|
||||
return http.upload<{ data: { url: string; fileName: string } }>('/monitor/account/splash-image/upload', formData)
|
||||
},
|
||||
|
||||
// 获取当前用户的开屏图片
|
||||
async getSplashImage(username: string) {
|
||||
return http.get<{ data: SplashImageResponse }>('/monitor/account/splash-image', { username })
|
||||
},
|
||||
|
||||
// 根据用户名获取开屏图片(用于启动时)
|
||||
async getSplashImageByUsername(username: string) {
|
||||
return http.get<{ data: SplashImageResponse }>('/monitor/account/splash-image/by-username', { username })
|
||||
},
|
||||
|
||||
// 删除自定义开屏图片(恢复默认)
|
||||
async deleteSplashImage(username: string) {
|
||||
return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`)
|
||||
},
|
||||
|
||||
// 上传品牌logo
|
||||
async uploadBrandLogo(file: File, username: string) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('username', username)
|
||||
return http.upload<{ data: { url: string; fileName: string } }>('/monitor/account/brand-logo/upload', formData)
|
||||
},
|
||||
|
||||
// 获取当前用户的品牌logo
|
||||
async getBrandLogo(username: string) {
|
||||
return http.get<{ data: { url: string } }>('/monitor/account/brand-logo', { username })
|
||||
},
|
||||
|
||||
// 删除品牌logo
|
||||
async deleteBrandLogo(username: string) {
|
||||
return http.post<{ data: string }>(`/monitor/account/brand-logo/delete?username=${username}`)
|
||||
},
|
||||
|
||||
// 获取全局开屏图片
|
||||
async getGlobalSplashImage() {
|
||||
return http.get<{ data: { url: string } }>('/monitor/account/global-splash-image')
|
||||
}
|
||||
}
|
||||
|
||||
13
electron-vue-template/src/renderer/api/system.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { http } from './http';
|
||||
|
||||
export const systemApi = {
|
||||
openGenmaiSpirit(accountId?: number | null) {
|
||||
const url = accountId ? `/api/system/genmai/open?accountId=${accountId}` : '/api/system/genmai/open';
|
||||
return http.post(url);
|
||||
},
|
||||
|
||||
clearCache() {
|
||||
return http.post('/api/system/cache/clear');
|
||||
}
|
||||
};
|
||||
|
||||
11
electron-vue-template/src/renderer/api/update.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { http } from './http'
|
||||
|
||||
export const updateApi = {
|
||||
getVersion() {
|
||||
return http.get('/api/system/version')
|
||||
},
|
||||
|
||||
checkUpdate(currentVersion: string) {
|
||||
return http.get(`/system/version/check?currentVersion=${currentVersion}`)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,32 @@
|
||||
// 斑马订单模型(根据页面所需字段精简定义)
|
||||
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;
|
||||
}
|
||||
import { http } from './http'
|
||||
|
||||
export interface ZebraOrdersResp {
|
||||
orders: ZebraOrder[];
|
||||
total?: number;
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
import { http } from './http';
|
||||
|
||||
export interface BanmaAccount {
|
||||
id?: number;
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
tokenExpireAt?: string | number;
|
||||
isDefault?: number;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 斑马 API:与原 zebra-api.js 对齐的接口封装
|
||||
export const zebraApi = {
|
||||
// 账号管理(ruoyi-admin)
|
||||
getAccounts() {
|
||||
return http.get<{ code?: number; msg?: string; data: BanmaAccount[] }>('/tool/banma/accounts');
|
||||
getAccounts(name?: string) {
|
||||
return http.get('/tool/banma/accounts', name ? { name } : undefined)
|
||||
},
|
||||
saveAccount(body: BanmaAccount) {
|
||||
return http.post<{ id: number }>('/tool/banma/accounts', body);
|
||||
|
||||
getAccountLimit(name?: string) {
|
||||
return http.get('/tool/banma/account-limit', name ? { name } : undefined)
|
||||
},
|
||||
|
||||
saveAccount(body: any, name?: string) {
|
||||
const url = name ? `/tool/banma/accounts?name=${encodeURIComponent(name)}` : '/tool/banma/accounts'
|
||||
return http.post(url, body)
|
||||
},
|
||||
|
||||
removeAccount(id: number) {
|
||||
// 用 postVoid 也可,但这里前端未用到,保留以备将来
|
||||
return http.delete<void>(`/tool/banma/accounts/${id}`);
|
||||
return http.delete(`/tool/banma/accounts/${id}`)
|
||||
},
|
||||
|
||||
// 业务采集
|
||||
getShops(params?: { accountId?: number }) {
|
||||
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>(
|
||||
'/api/banma/shops', params as unknown as Record<string, unknown>
|
||||
);
|
||||
},
|
||||
getOrders(params: { accountId?: number; startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string; batchId: string }) {
|
||||
return http.get<ZebraOrdersResp>('/api/banma/orders', params as unknown as Record<string, unknown>);
|
||||
return http.get('/api/banma/shops', params as Record<string, unknown>)
|
||||
},
|
||||
|
||||
// 其他功能(客户端微服务)
|
||||
getOrdersByBatch(batchId: string) {
|
||||
return http.get<ZebraOrdersResp>(`/api/banma/orders/batch/${batchId}`);
|
||||
getOrders(params: any, signal?: AbortSignal) {
|
||||
return http.get('/api/banma/orders', params as Record<string, unknown>, signal)
|
||||
},
|
||||
|
||||
getLatestOrders() {
|
||||
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
|
||||
},
|
||||
getOrderStats() {
|
||||
return http.get('/api/banma/orders/stats');
|
||||
},
|
||||
searchOrders(searchParams: Record<string, unknown>) {
|
||||
return http.get('/api/banma/orders/search', searchParams);
|
||||
},
|
||||
};
|
||||
return http.get('/api/banma/orders/latest')
|
||||
}
|
||||
}
|
||||
9
electron-vue-template/src/renderer/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
31
electron-vue-template/src/renderer/components.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, defineAsyncComponent } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { amazonApi } from '../../api/amazon'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
import { useFileDrop } from '../../composables/useFileDrop'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
|
||||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
updateData: [data: any[]]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableLoading = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
const progressVisible = ref(false)
|
||||
const localProductData = ref<any[]>([])
|
||||
const currentAsin = ref('')
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
const region = ref('JP')
|
||||
const regionOptions = [
|
||||
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
|
||||
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
|
||||
]
|
||||
const pendingAsins = ref<string[]>([])
|
||||
const selectedFileName = ref('')
|
||||
const amazonUpload = ref<HTMLInputElement | null>(null)
|
||||
const exportLoading = ref(false)
|
||||
const amazonExampleVisible = ref(false)
|
||||
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
||||
const vipStatus = inject<any>('vipStatus')
|
||||
|
||||
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
function removeSelectedFile() {
|
||||
selectedFileName.value = ''
|
||||
pendingAsins.value = []
|
||||
if (amazonUpload.value) {
|
||||
amazonUpload.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function processExcelFile(file: File) {
|
||||
try {
|
||||
loading.value = true
|
||||
progressPercentage.value = 0
|
||||
progressVisible.value = false
|
||||
|
||||
const response = await amazonApi.importAsinFromExcel(file)
|
||||
const asinList = response.data.asinList
|
||||
|
||||
if (!asinList || asinList.length === 0) {
|
||||
showMessage('文件中未找到有效的ASIN数据', 'warning')
|
||||
return
|
||||
}
|
||||
pendingAsins.value = asinList
|
||||
selectedFileName.value = file.name
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '处理文件失败', 'error')
|
||||
} finally {
|
||||
loading.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 = ''
|
||||
}
|
||||
|
||||
const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({
|
||||
accept: /\.xlsx?$/i,
|
||||
onFile: processExcelFile,
|
||||
onError: (msg) => showMessage(msg, 'warning')
|
||||
})
|
||||
|
||||
async function batchGetProductInfo(asinList: string[]) {
|
||||
if (refreshVipStatus) await refreshVipStatus()
|
||||
if (!props.isVip) {
|
||||
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
currentAsin.value = '正在处理...'
|
||||
progressPercentage.value = 0
|
||||
|
||||
const batchId = `BATCH_${Date.now()}`
|
||||
const batchSize = 2
|
||||
const totalBatches = Math.ceil(asinList.length / batchSize)
|
||||
let processedCount = 0
|
||||
|
||||
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, region.value, abortController?.signal)
|
||||
|
||||
if (result?.data?.products?.length > 0) {
|
||||
localProductData.value.push(...result.data.products)
|
||||
// 立即更新父组件数据,实时显示
|
||||
emit('updateData', [...localProductData.value])
|
||||
if (tableLoading.value) tableLoading.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') break
|
||||
console.error(`批次${i + 1}失败:`, error)
|
||||
}
|
||||
|
||||
processedCount += batchAsins.length
|
||||
progressPercentage.value = Math.round((processedCount / asinList.length) * 100)
|
||||
|
||||
if (i < totalBatches - 1 && loading.value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1500))
|
||||
}
|
||||
}
|
||||
|
||||
progressPercentage.value = 100
|
||||
currentAsin.value = '处理完成'
|
||||
} catch (error: any) {
|
||||
if (error.name !== 'AbortError') {
|
||||
showMessage(error.message || '批量获取产品信息失败', 'error')
|
||||
currentAsin.value = '处理失败'
|
||||
}
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startQueuedFetch() {
|
||||
if (!pendingAsins.value.length) {
|
||||
showMessage('请先导入ASIN列表', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
// 开始采集前先清空数据
|
||||
localProductData.value = []
|
||||
emit('updateData', [])
|
||||
|
||||
abortController = new AbortController()
|
||||
loading.value = true
|
||||
progressVisible.value = true
|
||||
tableLoading.value = true
|
||||
try {
|
||||
await batchGetProductInfo(pendingAsins.value)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
loading.value = false
|
||||
abortController = null
|
||||
}
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
if (!localProductData.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
|
||||
let html = `<table>
|
||||
<tr><th>ASIN</th><th>卖家/配送方</th><th>当前售价</th></tr>`
|
||||
|
||||
localProductData.value.forEach(product => {
|
||||
const sellerText = getSellerShipperText(product)
|
||||
html += `<tr>
|
||||
<td>${product.asin || ''}</td>
|
||||
<td>${sellerText}</td>
|
||||
<td>${product.price || '无货'}</td>
|
||||
</tr>`
|
||||
})
|
||||
html += '</table>'
|
||||
|
||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls`
|
||||
|
||||
const username = getUsernameFromToken()
|
||||
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
|
||||
|
||||
if (success) {
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
}
|
||||
exportLoading.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() {
|
||||
abortController?.abort()
|
||||
abortController = null
|
||||
loading.value = false
|
||||
currentAsin.value = '已停止'
|
||||
showMessage('已停止获取产品数据', 'info')
|
||||
}
|
||||
|
||||
function openAmazonUpload() {
|
||||
amazonUpload.value?.click()
|
||||
}
|
||||
|
||||
function viewAmazonExample() {
|
||||
amazonExampleVisible.value = true
|
||||
}
|
||||
|
||||
function downloadAmazonTemplate() {
|
||||
const html = '<table><tr><th>ASIN</th></tr><tr><td>B0XXXXXXX1</td></tr><tr><td>B0XXXXXXX2</td></tr></table>'
|
||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'amazon_asin_template.xls'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 组件挂载时加载缓存数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await amazonApi.getLatestProducts()
|
||||
if (resp.data?.products && resp.data.products.length > 0) {
|
||||
localProductData.value = resp.data.products
|
||||
emit('updateData', resp.data.products)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存数据失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loading,
|
||||
progressVisible,
|
||||
progressPercentage,
|
||||
localProductData
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="asin-panel">
|
||||
<div class="steps-flow">
|
||||
<!-- 1 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">1</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">导入ASIN</div></div>
|
||||
<div class="desc">仅支持包含 ASIN 列的 Excel 文档</div>
|
||||
<div class="links">
|
||||
<a class="link" @click.prevent="viewAmazonExample">点击查看示例</a>
|
||||
<span class="sep">|</span>
|
||||
<a class="link" @click.prevent="downloadAmazonTemplate">点击下载模板</a>
|
||||
</div>
|
||||
<div class="dropzone" :class="{ active: dragActive }" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openAmazonUpload">
|
||||
<div class="dz-el-icon">📤</div>
|
||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
||||
</div>
|
||||
<input ref="amazonUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleExcelUpload" :disabled="loading" />
|
||||
<div v-if="selectedFileName" class="file-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ selectedFileName }}</span>
|
||||
<span class="delete-btn" @click="removeSelectedFile" title="删除文件">🗑️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2 网站地区 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">2</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">网站地区</div></div>
|
||||
<div class="desc">请选择目标网站地区,如:日本区</div>
|
||||
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
|
||||
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 3 获取数据 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">3</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">获取数据</div></div>
|
||||
<div class="desc">导入表格后,点击下方按钮开始获取ASIN数据</div>
|
||||
<div class="action-buttons column">
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="!pendingAsins.length || loading" @click="startQueuedFetch">{{ loading ? '处理中...' : '获取数据' }}</el-button>
|
||||
<el-button size="small" class="w100" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 4 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">4</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">导出数据</div></div>
|
||||
<div class="action-buttons column">
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="!localProductData.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出Excel' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="amazonExampleVisible" title="示例 - ASIN文档格式" width="480px">
|
||||
<div>
|
||||
<div style="margin:8px 0;color:#606266;font-size:13px;">Excel 示例:</div>
|
||||
<el-table :data="[{asin:'B0XXXXXXX1'},{asin:'B0XXXXXXX2'}]" size="small" border>
|
||||
<el-table-column prop="asin" label="ASIN" />
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" class="btn-blue" @click="amazonExampleVisible = false">我知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.asin-panel {flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden;}
|
||||
.steps-flow {position: relative; flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none;}
|
||||
.asin-panel .steps-flow::-webkit-scrollbar {display: none;}
|
||||
.steps-flow:before {content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6);}
|
||||
.flow-item {position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0;}
|
||||
.flow-item .step-index {position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px;}
|
||||
.step-card {border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0;}
|
||||
.step-header {display: flex; align-items: center; gap: 8px; margin-bottom: 8px;}
|
||||
.title {font-size: 14px; font-weight: 600; color: #303133; text-align: left;}
|
||||
.desc {font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5;}
|
||||
.links {display: flex; align-items: center; gap: 2px; margin-bottom: 8px;}
|
||||
.link {color: #409EFF; cursor: pointer; font-size: 12px;}
|
||||
.sep {color: #dcdfe6;}
|
||||
.dropzone {border: 1px dashed #c0c4cc; border-radius: 6px; padding: 16px; text-align: center; cursor: pointer; background: #fafafa;}
|
||||
.dropzone:hover {background: #f6fbff; border-color: #409EFF;}
|
||||
.dz-el-icon {font-size: 18px; margin-bottom: 4px; color: #909399;}
|
||||
.dz-text {color: #303133; font-size: 13px;}
|
||||
.dz-sub {color: #909399; font-size: 12px;}
|
||||
.file-chip {display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box;}
|
||||
.file-chip .dot {width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0;}
|
||||
.file-chip .name {flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
|
||||
.file-chip .delete-btn {cursor: pointer; opacity: 0.6; flex-shrink: 0;}
|
||||
.file-chip .delete-btn:hover {opacity: 1;}
|
||||
.action-buttons.column {display: flex; flex-direction: column; gap: 8px;}
|
||||
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||
.w100 {width: 100%;}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, defineAsyncComponent } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { systemApi } from '../../api/system'
|
||||
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
|
||||
const AccountManager = defineAsyncComponent(() => import('../common/AccountManager.vue'))
|
||||
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
}>()
|
||||
|
||||
const genmaiLoading = ref(false)
|
||||
const genmaiAccounts = ref<GenmaiAccount[]>([])
|
||||
const selectedGenmaiAccountId = ref<number | null>(null)
|
||||
const showAccountManager = ref(false)
|
||||
const accountManagerRef = ref<any>(null)
|
||||
|
||||
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
async function openGenmaiSpirit() {
|
||||
if (!genmaiAccounts.value.length) {
|
||||
showAccountManager.value = true
|
||||
return
|
||||
}
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await systemApi.openGenmaiSpirit(selectedGenmaiAccountId.value)
|
||||
showMessage('跟卖精灵已打开', 'success')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGenmaiAccounts() {
|
||||
try {
|
||||
const res = await genmaiApi.getAccounts(getUsernameFromToken())
|
||||
genmaiAccounts.value = (res as any)?.data ?? []
|
||||
if (genmaiAccounts.value[0]) selectedGenmaiAccountId.value = genmaiAccounts.value[0].id
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadGenmaiAccounts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="genmai-panel">
|
||||
<div class="steps-flow">
|
||||
<!-- 1. 选择账号 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">1</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">需启动的跟卖精灵账号</div></div>
|
||||
<div class="desc">请选择需启动的跟卖精灵账号</div>
|
||||
<template v-if="genmaiAccounts.length">
|
||||
<el-scrollbar :class="['account-list', { 'scroll-limit': genmaiAccounts.length > 3 }]">
|
||||
<div>
|
||||
<div
|
||||
v-for="acc in genmaiAccounts"
|
||||
:key="acc.id"
|
||||
:class="['acct-item', { selected: selectedGenmaiAccountId === acc.id }]"
|
||||
@click="selectedGenmaiAccountId = acc.id"
|
||||
>
|
||||
<span class="acct-row">
|
||||
<span :class="['status-dot', acc.status === 1 ? 'on' : 'off']"></span>
|
||||
<img class="avatar" src="/image/user.png" alt="avatar" />
|
||||
<span class="acct-text">{{ acc.name || acc.username }}</span>
|
||||
<span v-if="selectedGenmaiAccountId === acc.id" class="acct-check">✔️</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="placeholder-box">
|
||||
<img class="placeholder-img" src="/icon/image.png" alt="add-account" />
|
||||
<div class="placeholder-tip">请添加跟卖精灵账号</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="step-actions btn-row">
|
||||
<el-button size="small" class="w50" @click="showAccountManager = true">添加账号</el-button>
|
||||
<el-button size="small" class="w50 btn-blue" @click="showAccountManager = true">账号管理</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2. 启动服务 -->
|
||||
<div class="flow-item">
|
||||
<div class="step-index">2</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">启动服务</div></div>
|
||||
<div class="desc">请确保设备已安装Chrome浏览器,否则服务将无法启动。打开跟卖精灵将关闭Chrome浏览器进程。</div>
|
||||
<div class="action-buttons column">
|
||||
<el-button
|
||||
size="small"
|
||||
class="w100 btn-blue"
|
||||
:disabled="genmaiLoading || !genmaiAccounts.length"
|
||||
@click="openGenmaiSpirit"
|
||||
>
|
||||
<span v-if="!genmaiLoading">启动服务</span>
|
||||
<span v-else><span class="inline-spinner">⟳</span> 启动中...</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccountManager
|
||||
ref="accountManagerRef"
|
||||
v-model="showAccountManager"
|
||||
platform="genmai"
|
||||
@refresh="loadGenmaiAccounts"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.genmai-panel {flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden;}
|
||||
.steps-flow {position: relative; flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none;}
|
||||
.genmai-panel .steps-flow::-webkit-scrollbar {display: none;}
|
||||
.steps-flow:before {content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6);}
|
||||
.flow-item {position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0;}
|
||||
.flow-item .step-index {position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px;}
|
||||
.step-card {border: none; border-radius: 0; padding: 0; background: transparent;}
|
||||
.step-header {display: flex; align-items: center; gap: 8px; margin-bottom: 8px;}
|
||||
.title {font-size: 14px; font-weight: 600; color: #303133; text-align: left;}
|
||||
.desc {font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5;}
|
||||
.account-list {height: auto;}
|
||||
.scroll-limit {max-height: 140px;}
|
||||
.placeholder-box {display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 8px;}
|
||||
.placeholder-img {width: 80px; opacity: 0.9;}
|
||||
.placeholder-tip {margin-top: 6px; font-size: 12px; color: #a8abb2;}
|
||||
.avatar {width: 18px; height: 18px; border-radius: 50%;}
|
||||
.acct-row {display: grid; grid-template-columns: 6px 18px 1fr auto; align-items: center; gap: 6px; width: 100%;}
|
||||
.acct-text {overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px;}
|
||||
.status-dot {width: 6px; height: 6px; border-radius: 50%; display: inline-block;}
|
||||
.status-dot.on {background: #22c55e;}
|
||||
.status-dot.off {background: #f87171;}
|
||||
.acct-item {padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px;}
|
||||
.acct-item.selected {background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff;}
|
||||
.acct-check {display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: transparent; color: #111; font-size: 12px;}
|
||||
.account-list::-webkit-scrollbar {width: 0; height: 0;}
|
||||
.step-actions {margin-top: 8px; display: flex; gap: 8px;}
|
||||
.btn-row {display: grid; grid-template-columns: 1fr 1fr; gap: 8px;}
|
||||
.w50 {width: 100%;}
|
||||
.action-buttons.column {display: flex; flex-direction: column; gap: 8px;}
|
||||
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||
.w100 {width: 100%;}
|
||||
.inline-spinner {display: inline-block; animation: spin 1s linear infinite;}
|
||||
@keyframes spin {0% { transform: rotate(0deg);}
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { authApi } from '../../api/auth'
|
||||
import { deviceApi } from '../../api/device'
|
||||
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||
import { splashApi } from '../../api/splash'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -11,8 +12,9 @@ interface Props {
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'loginSuccess', data: { token: string; user: any }): void
|
||||
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }): void
|
||||
(e: 'showRegister'): void
|
||||
(e: 'deviceConflict', username: string): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -31,22 +33,36 @@ async function handleAuth() {
|
||||
|
||||
authLoading.value = true
|
||||
try {
|
||||
// 1. 先检查设备限制
|
||||
await deviceApi.register({ username: authForm.value.username })
|
||||
|
||||
// 2. 设备检查通过,进行登录
|
||||
const data = await authApi.login(authForm.value)
|
||||
// 获取或生成设备ID
|
||||
const deviceId = await getOrCreateDeviceId()
|
||||
|
||||
// 登录
|
||||
const loginRes: any = await authApi.login({
|
||||
...authForm.value,
|
||||
clientId: deviceId
|
||||
})
|
||||
|
||||
// 保存开屏图片配置和品牌logo(不阻塞登录)
|
||||
saveSplashConfigInBackground(authForm.value.username)
|
||||
saveBrandLogoInBackground(authForm.value.username)
|
||||
|
||||
emit('loginSuccess', {
|
||||
token: data.token,
|
||||
user: {
|
||||
username: data.username,
|
||||
permissions: data.permissions
|
||||
}
|
||||
token: loginRes.data.accessToken || loginRes.data.token,
|
||||
permissions: loginRes.data.permissions,
|
||||
expireTime: loginRes.data.expireTime,
|
||||
accountType: loginRes.data.accountType,
|
||||
deviceTrialExpired: loginRes.data.deviceTrialExpired || false
|
||||
})
|
||||
ElMessage.success('登录成功')
|
||||
resetForm()
|
||||
} catch (err) {
|
||||
ElMessage.error((err as Error).message)
|
||||
} catch (err: any) {
|
||||
// 设备冲突/数量达上限:触发设备管理
|
||||
if (err.code === 501 ) {
|
||||
emit('deviceConflict', authForm.value.username)
|
||||
resetForm()
|
||||
} else {
|
||||
ElMessage.error(err.message || '登录失败')
|
||||
}
|
||||
} finally {
|
||||
authLoading.value = false
|
||||
}
|
||||
@@ -64,6 +80,31 @@ function resetForm() {
|
||||
function showRegister() {
|
||||
emit('showRegister')
|
||||
}
|
||||
|
||||
// 保存开屏图片配置
|
||||
async function saveSplashConfigInBackground(username: string) {
|
||||
try {
|
||||
const res = await splashApi.getSplashImage(username)
|
||||
const url = res?.data?.data?.url || res?.data?.url || ''
|
||||
await (window as any).electronAPI.saveSplashConfig(username, url)
|
||||
} catch (error) {
|
||||
console.error('[开屏图片] 保存配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存品牌logo配置
|
||||
async function saveBrandLogoInBackground(username: string) {
|
||||
try {
|
||||
const res = await splashApi.getBrandLogo(username)
|
||||
const url = res?.data?.url || ''
|
||||
// 保存到本地配置
|
||||
await (window as any).electronAPI.saveBrandLogoConfig(username, url)
|
||||
// 触发App.vue加载品牌logo
|
||||
window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: url }))
|
||||
} catch (error) {
|
||||
console.error('[品牌logo] 加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -99,6 +140,7 @@ function showRegister() {
|
||||
size="large"
|
||||
style="margin-bottom: 20px;"
|
||||
:disabled="authLoading"
|
||||
show-password
|
||||
@keyup.enter="handleAuth">
|
||||
</el-input>
|
||||
|
||||
@@ -124,36 +166,10 @@ function showRegister() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-logo {
|
||||
width: 160px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.auth-dialog {
|
||||
--el-color-primary: #1677FF;
|
||||
}
|
||||
|
||||
.auth-dialog :deep(.el-button--primary) {
|
||||
background-color: #1677FF;
|
||||
border-color: #1677FF;
|
||||
}
|
||||
|
||||
.auth-title-wrap {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f1f1f;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
text-align: left;
|
||||
}
|
||||
.auth-logo {width: 160px; height: auto;}
|
||||
.auth-dialog {--el-color-primary: #1677FF;}
|
||||
.auth-dialog :deep(.el-button--primary) {background-color: #1677FF; border-color: #1677FF;}
|
||||
.auth-title-wrap {margin-bottom: 12px;}
|
||||
.auth-title {margin: 0; font-size: 18px; font-weight: 700; color: #1f1f1f; text-align: left;}
|
||||
.auth-subtitle {margin: 6px 0 0; font-size: 12px; color: #8c8c8c; text-align: left;}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { authApi } from '../../api/auth'
|
||||
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -10,7 +11,7 @@ interface Props {
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'loginSuccess', data: { token: string; user: any }): void
|
||||
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }): void
|
||||
(e: 'backToLogin'): void
|
||||
}
|
||||
|
||||
@@ -34,6 +35,10 @@ const canRegister = computed(() => {
|
||||
usernameCheckResult.value === true
|
||||
})
|
||||
|
||||
function filterUsername(value: string) {
|
||||
registerForm.value.username = value.replace(/[^a-zA-Z0-9_]/g, '')
|
||||
}
|
||||
|
||||
async function checkUsernameAvailability() {
|
||||
if (!registerForm.value.username) {
|
||||
usernameCheckResult.value = null
|
||||
@@ -41,8 +46,9 @@ async function checkUsernameAvailability() {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await authApi.checkUsername(registerForm.value.username)
|
||||
usernameCheckResult.value = data.available
|
||||
const res: any = await authApi.checkUsername(registerForm.value.username)
|
||||
// 后端返回 {code: 200, data: true/false},data 直接是布尔值
|
||||
usernameCheckResult.value = res.data
|
||||
} catch {
|
||||
usernameCheckResult.value = null
|
||||
}
|
||||
@@ -53,24 +59,30 @@ async function handleRegister() {
|
||||
|
||||
registerLoading.value = true
|
||||
try {
|
||||
// 1. 注册
|
||||
await authApi.register({
|
||||
// 获取设备ID
|
||||
const deviceId = await getOrCreateDeviceId()
|
||||
|
||||
// 注册账号(传递设备ID用于判断是否赠送VIP)
|
||||
const registerRes: any = await authApi.register({
|
||||
username: registerForm.value.username,
|
||||
password: registerForm.value.password
|
||||
})
|
||||
|
||||
// 2. 注册成功后直接登录
|
||||
const loginData = await authApi.login({
|
||||
username: registerForm.value.username,
|
||||
password: registerForm.value.password
|
||||
password: registerForm.value.password,
|
||||
deviceId: deviceId
|
||||
})
|
||||
|
||||
// 显示注册成功提示
|
||||
if (registerRes.data.deviceTrialExpired) {
|
||||
ElMessage.warning('注册成功!您获得了3天VIP体验,但该设备试用期已过,请更换设备或联系管理员续费')
|
||||
} else {
|
||||
ElMessage.success('注册成功!您获得了3天VIP体验')
|
||||
}
|
||||
|
||||
// 使用注册返回的token直接登录
|
||||
emit('loginSuccess', {
|
||||
token: loginData.token,
|
||||
user: {
|
||||
username: loginData.username,
|
||||
permissions: loginData.permissions
|
||||
}
|
||||
token: registerRes.data.accessToken || registerRes.data.token,
|
||||
permissions: registerRes.data.permissions,
|
||||
expireTime: registerRes.data.expireTime,
|
||||
accountType: registerRes.data.accountType,
|
||||
deviceTrialExpired: registerRes.data.deviceTrialExpired || false
|
||||
})
|
||||
resetForm()
|
||||
} catch (err) {
|
||||
@@ -115,10 +127,11 @@ function backToLogin() {
|
||||
|
||||
<el-input
|
||||
v-model="registerForm.username"
|
||||
placeholder="请输入用户名"
|
||||
placeholder="请输入用户名(字母、数字、下划线)"
|
||||
size="large"
|
||||
style="margin-bottom: 15px;"
|
||||
:disabled="registerLoading"
|
||||
@input="filterUsername"
|
||||
@blur="checkUsernameAvailability">
|
||||
</el-input>
|
||||
|
||||
@@ -137,7 +150,8 @@ function backToLogin() {
|
||||
type="password"
|
||||
size="large"
|
||||
style="margin-bottom: 15px;"
|
||||
:disabled="registerLoading">
|
||||
:disabled="registerLoading"
|
||||
show-password>
|
||||
</el-input>
|
||||
|
||||
<el-input
|
||||
@@ -146,7 +160,8 @@ function backToLogin() {
|
||||
type="password"
|
||||
size="large"
|
||||
style="margin-bottom: 20px;"
|
||||
:disabled="registerLoading">
|
||||
:disabled="registerLoading"
|
||||
show-password>
|
||||
</el-input>
|
||||
|
||||
<div>
|
||||
@@ -170,36 +185,10 @@ function backToLogin() {
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.auth-logo {
|
||||
width: 160px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.auth-dialog {
|
||||
--el-color-primary: #1677FF;
|
||||
}
|
||||
|
||||
.auth-dialog :deep(.el-button--primary) {
|
||||
background-color: #1677FF;
|
||||
border-color: #1677FF;
|
||||
}
|
||||
|
||||
.auth-title-wrap {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f1f1f;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
text-align: left;
|
||||
}
|
||||
.auth-logo {width: 160px; height: auto;}
|
||||
.auth-dialog {--el-color-primary: #1677FF;}
|
||||
.auth-dialog :deep(.el-button--primary) {background-color: #1677FF; border-color: #1677FF;}
|
||||
.auth-title-wrap {margin-bottom: 12px;}
|
||||
.auth-title {margin: 0; font-size: 18px; font-weight: 700; color: #1f1f1f; text-align: left;}
|
||||
.auth-subtitle {margin: 6px 0 0; font-size: 12px; color: #8c8c8c; text-align: left;}
|
||||
</style>
|
||||
@@ -1,27 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, defineAsyncComponent, watch } from 'vue'
|
||||
import { zebraApi, type BanmaAccount } from '../../api/zebra'
|
||||
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
|
||||
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon'
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('./TrialExpiredDialog.vue'))
|
||||
|
||||
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon' | 'genmai'
|
||||
const props = defineProps<{ modelValue: boolean; platform?: PlatformKey }>()
|
||||
const emit = defineEmits(['update:modelValue', 'add', 'refresh'])
|
||||
const emit = defineEmits(['update:modelValue', 'refresh'])
|
||||
const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) })
|
||||
const curPlatform = ref<PlatformKey>(props.platform || 'zebra')
|
||||
|
||||
// 监听弹框打开,同步平台并加载数据
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal && props.platform) {
|
||||
curPlatform.value = props.platform
|
||||
load()
|
||||
}
|
||||
})
|
||||
|
||||
// 升级订阅弹框
|
||||
const showUpgradeDialog = ref(false)
|
||||
const PLATFORM_LABEL: Record<PlatformKey, string> = {
|
||||
zebra: '斑马 ERP',
|
||||
shopee: 'Shopee 虾皮购物',
|
||||
rakuten: 'Rakuten 乐天购物',
|
||||
amazon: 'Amazon 亚马逊'
|
||||
amazon: 'Amazon 亚马逊',
|
||||
genmai: '跟卖精灵'
|
||||
}
|
||||
|
||||
const accounts = ref<BanmaAccount[]>([])
|
||||
const accounts = ref<(BanmaAccount | GenmaiAccount)[]>([])
|
||||
const accountLimit = ref({ limit: 1, count: 0 })
|
||||
|
||||
// 添加账号对话框
|
||||
const accountDialogVisible = ref(false)
|
||||
const formUsername = ref('')
|
||||
const formPassword = ref('')
|
||||
|
||||
async function load() {
|
||||
// 目前后端只有斑马接口,其它平台先共用此接口占位
|
||||
const res = await zebraApi.getAccounts()
|
||||
const list = (res as any)?.data ?? res
|
||||
accounts.value = Array.isArray(list) ? list : []
|
||||
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
|
||||
const username = getUsernameFromToken()
|
||||
const [res, limitRes] = await Promise.all([api.getAccounts(username), api.getAccountLimit(username)])
|
||||
accounts.value = (res as any)?.data ?? res
|
||||
accountLimit.value = (limitRes as any)?.data ?? limitRes
|
||||
}
|
||||
|
||||
// 暴露方法供父组件调用
|
||||
@@ -43,11 +66,39 @@ async function onDelete(a: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除账号 "${a?.name || a?.username || id}" 吗?`, '提示', { type: 'warning' })
|
||||
} catch { return }
|
||||
await zebraApi.removeAccount(id)
|
||||
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
|
||||
await api.removeAccount(id)
|
||||
ElMessage({ message: '删除成功', type: 'success' })
|
||||
await load()
|
||||
emit('refresh') // 通知外层组件刷新账号列表
|
||||
}
|
||||
|
||||
async function handleAddAccount() {
|
||||
if (accountLimit.value.count >= accountLimit.value.limit) {
|
||||
ElMessage({ message: `账号数量已达上限`, type: 'warning' })
|
||||
return
|
||||
}
|
||||
formUsername.value = ''
|
||||
formPassword.value = ''
|
||||
accountDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitAccount() {
|
||||
const api = curPlatform.value === 'genmai' ? genmaiApi : zebraApi
|
||||
try {
|
||||
await api.saveAccount({
|
||||
username: formUsername.value,
|
||||
password: formPassword.value,
|
||||
status: 1
|
||||
}, getUsernameFromToken())
|
||||
ElMessage({ message: '添加成功', type: 'success' })
|
||||
accountDialogVisible.value = false
|
||||
await load()
|
||||
emit('refresh')
|
||||
} catch (e: any) {
|
||||
ElMessage({ message: e.message || '添加失败', type: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -64,8 +115,9 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
<div class="layout">
|
||||
<aside class="sider">
|
||||
<div class="sider-title">全账号管理</div>
|
||||
<div class="nav only-zebra">
|
||||
<div class="nav">
|
||||
<div :class="['nav-item', {active: curPlatform==='zebra'}]" @click="switchPlatform('zebra')">斑马 ERP</div>
|
||||
<div :class="['nav-item', {active: curPlatform==='genmai'}]" @click="switchPlatform('genmai')">跟卖精灵</div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="content">
|
||||
@@ -73,10 +125,10 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
<div class="top">
|
||||
<img src="/icon/image.png" class="hero" alt="logo" />
|
||||
<div class="head-main">
|
||||
<div class="main-title">在线账号管理(3/3)</div>
|
||||
<div class="main-title">在线账号管理({{ accountLimit.count }}/{{ accountLimit.limit }})</div>
|
||||
<div class="main-sub">
|
||||
您当前订阅可同时托管3家 Shopee 店铺<br>
|
||||
如需扩增同时托管店铺数,请 <span class="upgrade">升级订阅</span>。
|
||||
您当前订阅可同时托管{{ accountLimit.limit }}个{{ curPlatform === 'genmai' ? '跟卖精灵' : '斑马' }}账号<br>
|
||||
<span v-if="accountLimit.limit < 3">如需扩增账号数量,请 <span class="upgrade" @click="showUpgradeDialog = true">升级订阅</span>。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +136,7 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
<div v-for="a in accounts" :key="a.id" class="row">
|
||||
<span :class="['dot', a.status === 1 ? 'on' : 'off']"></span>
|
||||
<div class="user-info">
|
||||
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" />
|
||||
<img class="avatar" src="/image/user.png" />
|
||||
<span class="name">{{ a.name || a.username }}</span>
|
||||
</div>
|
||||
<span class="date">{{ formatDate(a) }}</span>
|
||||
@@ -92,44 +144,75 @@ export default defineComponent({ name: 'AccountManager' })
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<el-button type="primary" class="btn" @click="$emit('add')">添加账号</el-button>
|
||||
<el-button type="primary" class="btn" @click="handleAddAccount">添加账号</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 添加账号对话框 -->
|
||||
<el-dialog v-model="accountDialogVisible" width="420px" class="add-account-dialog">
|
||||
<template #header>
|
||||
<div class="aad-header">
|
||||
<img class="aad-icon" src="/icon/image.png" alt="logo" />
|
||||
<div class="aad-title">添加{{ curPlatform === 'genmai' ? '跟卖精灵' : '斑马' }}账号</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="aad-row">
|
||||
<el-input v-model="formUsername" :placeholder="curPlatform === 'genmai' ? '请输入账号(nickname)' : '请输入账号'" />
|
||||
</div>
|
||||
<div class="aad-row">
|
||||
<el-input v-model="formPassword" placeholder="请输入密码" type="password" show-password />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">添加</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 升级订阅弹框 -->
|
||||
<TrialExpiredDialog v-model="showUpgradeDialog" expired-type="subscribe" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.acc-manager :deep(.el-dialog__header) { text-align:center; }
|
||||
.layout { display:grid; grid-template-columns: 160px 1fr; gap: 12px; min-height: 340px; }
|
||||
.sider { border-right: 1px solid #ebeef5; padding-right: 10px; }
|
||||
.sider-title { color:#303133; font-size:13px; font-weight: 600; margin-bottom: 10px; text-align: left; }
|
||||
.nav { display:flex; flex-direction: column; gap: 4px; }
|
||||
.nav-item { padding: 6px 8px; border-radius: 4px; cursor: pointer; color:#606266; font-size: 12px; transition: all 0.2s; text-align: left; }
|
||||
.nav-item:hover { background:#f0f2f5; }
|
||||
.nav-item.active { background:#e6f4ff; color:#409EFF; font-weight: 600; }
|
||||
.platform-bar { font-weight: 600; color:#303133; margin: 0 0 12px 0; text-align: left; font-size: 14px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; }
|
||||
.content { display:flex; flex-direction: column; min-width: 0; }
|
||||
.top { display:flex; flex-direction: column; align-items:center; gap: 6px; margin-bottom: 12px; }
|
||||
.hero { width: 160px; height: auto; }
|
||||
.head-main { text-align:center; }
|
||||
.main-title { font-size: 16px; font-weight: 600; color:#303133; margin-bottom: 4px; }
|
||||
.main-sub { color:#909399; font-size: 11px; line-height: 1.4; }
|
||||
.upgrade { color:#409EFF; cursor: pointer; }
|
||||
.list { border:1px solid #ebeef5; border-radius: 6px; background: #fff; flex: 0 0 auto; width: 100%; max-height: 160px; overflow-y: auto; }
|
||||
.list.compact { max-height: 48px; }
|
||||
.row { display:grid; grid-template-columns: 8px 1fr 120px 60px; gap: 8px; align-items:center; padding: 4px 8px; border-bottom: 1px solid #f5f5f5; height: 28px; }
|
||||
.row:last-child { border-bottom:none; }
|
||||
.row:hover { background:#fafafa; }
|
||||
.dot { width:6px; height:6px; border-radius:50%; justify-self: center; }
|
||||
.dot.on { background:#52c41a; }
|
||||
.dot.off { background:#ff4d4f; }
|
||||
.user-info { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.avatar { width:22px; height:22px; border-radius:50%; object-fit: cover; }
|
||||
.name { font-weight:500; font-size: 13px; color:#303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.date { color:#999; font-size:11px; text-align: center; }
|
||||
.footer { display:flex; justify-content:center; padding-top: 10px; }
|
||||
.btn { width: 180px; height: 32px; font-size: 13px; }
|
||||
.acc-manager :deep(.el-dialog__header) {text-align:center;}
|
||||
.layout {display:grid; grid-template-columns: 160px 1fr; gap: 12px; min-height: 340px;}
|
||||
.sider {border-right: 1px solid #ebeef5; padding-right: 10px;}
|
||||
.sider-title {color:#303133; font-size:13px; font-weight: 600; margin-bottom: 10px; text-align: left;}
|
||||
.nav {display:flex; flex-direction: column; gap: 4px;}
|
||||
.nav-item {padding: 6px 8px; border-radius: 4px; cursor: pointer; color:#606266; font-size: 12px; transition: all 0.2s; text-align: left;}
|
||||
.nav-item:hover {background:#f0f2f5;}
|
||||
.nav-item.active {background:#e6f4ff; color:#409EFF; font-weight: 600;}
|
||||
.platform-bar {font-weight: 600; color:#303133; margin: 0 0 12px 0; text-align: left; font-size: 14px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5;}
|
||||
.content {display:flex; flex-direction: column; min-width: 0;}
|
||||
.top {display:flex; flex-direction: column; align-items:center; gap: 6px; margin-bottom: 12px;}
|
||||
.hero {width: 160px; height: auto;}
|
||||
.head-main {text-align:center;}
|
||||
.main-title {font-size: 16px; font-weight: 600; color:#303133; margin-bottom: 4px;}
|
||||
.main-sub {color:#909399; font-size: 11px; line-height: 1.4;}
|
||||
.upgrade {color:#409EFF; cursor: pointer; font-weight: 600; transition: all 0.2s ease;}
|
||||
.upgrade:hover {color:#0d5ed6; text-decoration: underline;}
|
||||
.list {border:1px solid #ebeef5; border-radius: 6px; background: #fff; flex: 0 0 auto; width: 100%; max-height: 160px; overflow-y: auto;}
|
||||
.list.compact {max-height: 48px;}
|
||||
/* 添加账号对话框样式 */
|
||||
.add-account-dialog .aad-header {display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%;}
|
||||
.add-account-dialog .aad-icon {width: 120px; height: auto;}
|
||||
.add-account-dialog .aad-title {font-weight: 600; font-size: 18px; text-align: center;}
|
||||
.add-account-dialog .aad-row {margin-top: 12px;}
|
||||
:deep(.add-account-dialog .el-dialog__header) {text-align: center; padding-right: 0; display: block;}
|
||||
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||
.btn-blue:hover {background: #0d5ed6; border-color: #0d5ed6;}
|
||||
.row {display:grid; grid-template-columns: 8px 1fr 120px 60px; gap: 8px; align-items:center; padding: 4px 8px; border-bottom: 1px solid #f5f5f5; height: 28px;}
|
||||
.row:last-child {border-bottom:none;}
|
||||
.row:hover {background:#fafafa;}
|
||||
.dot {width:6px; height:6px; border-radius:50%; justify-self: center;}
|
||||
.dot.on {background:#52c41a;}
|
||||
.dot.off {background:#ff4d4f;}
|
||||
.user-info {display: flex; align-items: center; gap: 8px; min-width: 0;}
|
||||
.avatar {width:22px; height:22px; border-radius:50%; object-fit: cover;}
|
||||
.name {font-weight:500; font-size: 13px; color:#303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
|
||||
.date {color:#999; font-size:11px; text-align: center;}
|
||||
.footer {display:flex; justify-content:center; padding-top: 10px;}
|
||||
.btn {width: 180px; height: 32px; font-size: 13px;}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
expiredType: 'device' | 'account' | 'both' | 'subscribe' // 设备过期、账号过期、都过期、主动订阅
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const titleText = computed(() => {
|
||||
if (props.expiredType === 'subscribe') return '订阅服务'
|
||||
if (props.expiredType === 'both') return '试用已到期'
|
||||
if (props.expiredType === 'account') return '账号试用已到期'
|
||||
return '设备试用已到期'
|
||||
})
|
||||
|
||||
const subtitleText = computed(() => {
|
||||
if (props.expiredType === 'subscribe') return '联系客服订阅或续费,享受完整服务'
|
||||
if (props.expiredType === 'both') return '试用已到期,请联系客服订阅以获取完整服务'
|
||||
if (props.expiredType === 'account') return '账号试用已到期,请联系客服订阅'
|
||||
return '当前设备试用已到期,请更换新设备体验或联系客服订阅'
|
||||
})
|
||||
|
||||
function handleConfirm() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function copyWechat() {
|
||||
navigator.clipboard.writeText('butaihaoba001').then(() => {
|
||||
ElMessage.success('微信号已复制')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="true"
|
||||
width="380px"
|
||||
center
|
||||
class="trial-expired-dialog">
|
||||
<div class="expired-content">
|
||||
<!-- Logo -->
|
||||
<div style="text-align: center; margin-bottom: 16px;">
|
||||
<img src="/icon/image.png" alt="logo" class="expired-logo" />
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h2 class="expired-title">{{ titleText }}</h2>
|
||||
|
||||
<!-- 副标题 -->
|
||||
<p class="expired-subtitle">{{ subtitleText }}</p>
|
||||
|
||||
<!-- 客服微信 -->
|
||||
<div class="wechat-card" @click="copyWechat">
|
||||
<div class="wechat-icon">
|
||||
<svg viewBox="0 0 1024 1024">
|
||||
<path d="M664.250054 368.541681c10.015098 0 19.892049 0.732687 29.67281 1.795902-26.647917-122.810047-159.358451-214.077703-310.826188-214.077703-169.353083 0-308.085774 114.232694-308.085774 259.274068 0 83.708494 46.165436 152.460344 123.281791 205.78483l-30.80868 91.730191 107.688651-53.455469c38.558178 7.53665 69.459978 15.308661 107.924012 15.308661 9.66308 0 19.230993-0.470721 28.752858-1.225921-6.025227-20.36584-9.521864-41.723264-9.521864-63.94508C402.328693 476.632491 517.908058 368.541681 664.250054 368.541681zM498.62897 285.87389c23.200398 0 38.557154 15.120372 38.557154 38.061874 0 22.846334-15.356756 38.298144-38.557154 38.298144-23.107277 0-46.260603-15.45181-46.260603-38.298144C452.368366 300.994262 475.522716 285.87389 498.62897 285.87389zM283.016307 362.23394c-23.107277 0-46.402843-15.45181-46.402843-38.298144 0-22.941502 23.295566-38.061874 46.402843-38.061874 23.081695 0 38.46301 15.120372 38.46301 38.061874C321.479317 346.78213 306.098002 362.23394 283.016307 362.23394zM945.448458 606.151333c0-121.888048-123.258255-221.236753-261.683954-221.236753-146.57838 0-262.015505 99.348706-262.015505 221.236753 0 122.06508 115.437126 221.200938 262.015505 221.200938 30.66644 0 61.617359-7.609305 92.423993-15.262612l84.513836 45.786813-23.178909-76.17082C899.379213 735.776599 945.448458 674.90216 945.448458 606.151333zM598.803483 567.994292c-15.332197 0-30.807656-15.096836-30.807656-30.501688 0-15.190981 15.47546-30.477129 30.807656-30.477129 23.295566 0 38.558178 15.286148 38.558178 30.477129C637.361661 552.897456 622.099049 567.994292 598.803483 567.994292zM768.25071 567.994292c-15.213493 0-30.594809-15.096836-30.594809-30.501688 0-15.190981 15.381315-30.477129 30.594809-30.477129 23.107277 0 38.558178 15.286148 38.558178 30.477129C806.808888 552.897456 791.357987 567.994292 768.25071 567.994292z" fill="#09BB07"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="wechat-info">
|
||||
<div class="wechat-label">客服微信</div>
|
||||
<div class="wechat-id">butaihaoba001</div>
|
||||
</div>
|
||||
<div class="copy-icon">📋</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
class="confirm-btn"
|
||||
@click="handleConfirm"
|
||||
style="width: 100%;">
|
||||
我知道了
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.trial-expired-dialog :deep(.el-dialog) {border-radius: 16px;}
|
||||
.trial-expired-dialog :deep(.el-dialog__header) {padding: 0; margin: 0;}
|
||||
.trial-expired-dialog :deep(.el-dialog__body) {padding: 20px;}
|
||||
.expired-content {display: flex; flex-direction: column; align-items: center; padding: 10px 0;}
|
||||
.expired-logo {width: 160px; height: auto;}
|
||||
.expired-title {font-size: 18px; font-weight: 700; color: #1f1f1f; margin: 0 0 8px 0; text-align: center;}
|
||||
.expired-subtitle {font-size: 12px; color: #8c8c8c; margin: 0 0 20px 0; text-align: center; line-height: 1.5;}
|
||||
.wechat-card {display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: #f5f5f5; border-radius: 6px; margin-bottom: 20px; width: 90%; cursor: pointer; transition: all 0.3s; position: relative;}
|
||||
.wechat-card:hover {background: #e8f5e9; box-shadow: 0 2px 8px rgba(9, 187, 7, 0.15);}
|
||||
.wechat-icon {flex-shrink: 0;}
|
||||
.wechat-icon svg {width: 36px; height: 36px;}
|
||||
.wechat-info {flex: 1; text-align: left;}
|
||||
.wechat-label {font-size: 12px; color: #666; margin-bottom: 2px;}
|
||||
.wechat-id {font-size: 15px; font-weight: 500; color: #1f1f1f;}
|
||||
.copy-icon {margin-left: auto; font-size: 16px; opacity: 0.5; transition: all 0.3s;}
|
||||
.wechat-card:hover .copy-icon {opacity: 1; transform: scale(1.1);}
|
||||
.confirm-btn {height: 40px; font-size: 14px; font-weight: 500; background: #1677FF; border-color: #1677FF; border-radius: 6px;}
|
||||
.confirm-btn:hover {background: #4096ff; border-color: #4096ff;}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="version-info" @click="autoCheck">v{{ version || '-' }}</div>
|
||||
|
||||
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center class="update-dialog" :title="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
|
||||
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center
|
||||
:class="['update-dialog', `stage-${stage}`]"
|
||||
:title="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
|
||||
<div v-if="stage === 'check'" class="update-content">
|
||||
<div class="update-layout">
|
||||
<div class="left-pane">
|
||||
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon" />
|
||||
<img src="/icon/icon1.png" class="app-icon app-icon-large" alt="App Icon"/>
|
||||
</div>
|
||||
<div class="right-pane">
|
||||
<p class="announce">新版本的"{{ appName }}"已经发布</p>
|
||||
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装,您现在的版本是 {{ version }},要现在安装吗?</p>
|
||||
|
||||
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装,您现在的版本是 {{
|
||||
version
|
||||
}},要现在安装吗?</p>
|
||||
<div class="update-details form">
|
||||
<h4>更新信息</h4>
|
||||
<el-input
|
||||
@@ -20,16 +21,15 @@
|
||||
class="notes-box"
|
||||
:rows="6"
|
||||
readonly
|
||||
resize="none" />
|
||||
resize="none"/>
|
||||
</div>
|
||||
|
||||
<div class="update-actions row">
|
||||
<div class="update-buttons">
|
||||
<div class="left-actions">
|
||||
<el-button size="small" @click="show=false">跳过这个版本</el-button>
|
||||
<el-button size="small" @click="skipVersion">跳过这个版本</el-button>
|
||||
</div>
|
||||
<div class="right-actions">
|
||||
<el-button size="small" @click="show=false">稍后提醒</el-button>
|
||||
<el-button size="small" @click="remindLater">稍后提醒</el-button>
|
||||
<el-button size="small" type="primary" @click="start">下载更新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,18 +41,18 @@
|
||||
<div v-else-if="stage === 'downloading'" class="update-content">
|
||||
<div class="download-main">
|
||||
<div class="download-icon">
|
||||
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
|
||||
<img src="/icon/icon1.png" class="app-icon" alt="App Icon"/>
|
||||
</div>
|
||||
<div class="download-content">
|
||||
<div class="download-info">
|
||||
<p>正在下载更新</p>
|
||||
<p>正在下载安装...</p>
|
||||
</div>
|
||||
<div class="download-progress">
|
||||
<el-progress
|
||||
:percentage="prog.percentage"
|
||||
:show-text="false"
|
||||
:stroke-width="6"
|
||||
color="#409EFF" />
|
||||
color="#409EFF"/>
|
||||
<div class="progress-details">
|
||||
<span style="font-weight: 500">{{ prog.current }} / {{ prog.total }}</span>
|
||||
<el-button size="small" @click="cancelDownload">取消</el-button>
|
||||
@@ -61,28 +61,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="stage === 'completed'" class="update-content">
|
||||
<div class="update-header text-center">
|
||||
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
|
||||
<h3>更新完成</h3>
|
||||
<p>更新文件已下载,将在重启后自动应用</p>
|
||||
</div>
|
||||
|
||||
<div class="download-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ prog.current }} / {{ prog.total }}</span>
|
||||
<div class="download-main">
|
||||
<div class="download-icon">
|
||||
<img src="/icon/icon1.png" class="app-icon" alt="App Icon"/>
|
||||
</div>
|
||||
<div class="download-content">
|
||||
<div class="download-info">
|
||||
<p>可以开始安装了</p>
|
||||
</div>
|
||||
<div class="download-progress">
|
||||
<el-progress
|
||||
:percentage="100"
|
||||
:show-text="false"
|
||||
:stroke-width="6"
|
||||
color="#67C23A"/>
|
||||
<div class="progress-details">
|
||||
<span style="font-weight: 500" v-if="prog.current !== '0 MB' && prog.total !== '0 MB'">{{ prog.current }} / {{ prog.total }}</span>
|
||||
<span style="font-weight: 500" v-else>下载完成</span>
|
||||
<div class="action-buttons">
|
||||
<el-button size="small" @click="clearDownloadedFiles">清除下载</el-button>
|
||||
<el-button size="small" type="primary" @click="installUpdate">立即重启</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="100"
|
||||
:show-text="false"
|
||||
:stroke-width="6"
|
||||
color="#67C23A" />
|
||||
</div>
|
||||
|
||||
<div class="update-buttons">
|
||||
<el-button @click="cancelDownload">稍后更新</el-button>
|
||||
<el-button type="primary" @click="installUpdate">重启应用新版本</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -90,13 +93,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { updateApi } from '../../api/update'
|
||||
import {ref, computed, onMounted, onUnmounted, watch} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {updateApi} from '../../api/update'
|
||||
import {getSettings} from '../../utils/settings'
|
||||
import {getUsernameFromToken} from '../../utils/token'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
defineExpose({ checkForUpdatesNow })
|
||||
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
@@ -105,360 +112,244 @@ const show = computed({
|
||||
type Stage = 'check' | 'downloading' | 'completed'
|
||||
const stage = ref<Stage>('check')
|
||||
const appName = ref('我了个电商')
|
||||
const version = ref('2.0.0')
|
||||
const prog = ref({ percentage: 0, current: '0 MB', total: '0 MB', speed: '' })
|
||||
const version = ref('')
|
||||
const prog = ref({percentage: 0, current: '0 MB', total: '0 MB'})
|
||||
const info = ref({
|
||||
latestVersion: '2.4.8',
|
||||
downloadUrl: '',
|
||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 增加了新的功能模块\n• 优化了数据处理性能',
|
||||
currentVersion: '',
|
||||
hasUpdate: false
|
||||
latestVersion: '',
|
||||
asarUrl: '',
|
||||
jarUrl: '',
|
||||
updateNotes: '',
|
||||
currentVersion: ''
|
||||
})
|
||||
|
||||
async function autoCheck() {
|
||||
async function checkUpdate(silent = false) {
|
||||
try {
|
||||
ElMessage({ message: '正在检查更新...', type: 'info' })
|
||||
|
||||
try {
|
||||
version.value = await updateApi.getVersion()
|
||||
} catch (error) {
|
||||
console.error('获取版本失败:', error)
|
||||
version.value = '2.0.0'
|
||||
}
|
||||
version.value = await (window as any).electronAPI.getJarVersion()
|
||||
const result = (await updateApi.checkUpdate(version.value))?.data
|
||||
|
||||
info.value = {
|
||||
currentVersion: version.value,
|
||||
latestVersion: '2.4.9',
|
||||
downloadUrl: 'https://qiniu.pxdj.tashowz.com/2025/09/becac13811214c909d11162d2ff2c863.asar',
|
||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 轻量级更新(仅替换app.asar)',
|
||||
hasUpdate: true
|
||||
currentVersion: result.currentVersion || version.value,
|
||||
latestVersion: result.latestVersion || version.value,
|
||||
asarUrl: result.asarUrl || '',
|
||||
jarUrl: result.jarUrl || '',
|
||||
updateNotes: result.updateNotes || ''
|
||||
}
|
||||
|
||||
if (!result.needUpdate) {
|
||||
if (!silent) ElMessage.info('当前已是最新版本')
|
||||
return
|
||||
}
|
||||
|
||||
if (localStorage.getItem('skipped_version') === result.latestVersion) return
|
||||
|
||||
const remindTime = localStorage.getItem('remind_later_time')
|
||||
if (remindTime && Date.now() < parseInt(remindTime)) return
|
||||
|
||||
if (getSettings(getUsernameFromToken()).autoUpdate) {
|
||||
await downloadUpdate()
|
||||
return
|
||||
}
|
||||
|
||||
show.value = true
|
||||
stage.value = 'check'
|
||||
ElMessage({ message: '发现新版本', type: 'success' })
|
||||
if (!silent) ElMessage.success('发现新版本')
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
ElMessage({ message: '检查更新失败', type: 'error' })
|
||||
if (!silent) ElMessage.error('检查更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdatesNow() {
|
||||
if (stage.value === 'downloading' || stage.value === 'completed') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
await checkUpdate(false)
|
||||
}
|
||||
|
||||
function skipVersion() {
|
||||
localStorage.setItem('skipped_version', info.value.latestVersion)
|
||||
show.value = false
|
||||
}
|
||||
|
||||
function remindLater() {
|
||||
localStorage.setItem('remind_later_time', (Date.now() + 24 * 60 * 60 * 1000).toString())
|
||||
show.value = false
|
||||
}
|
||||
|
||||
async function start() {
|
||||
if (!info.value.downloadUrl) {
|
||||
ElMessage({ message: '下载链接不可用', type: 'error' });
|
||||
return;
|
||||
if (stage.value !== 'check') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
await downloadUpdate(true)
|
||||
}
|
||||
|
||||
async function downloadUpdate(showDialog = false) {
|
||||
if (!info.value.asarUrl && !info.value.jarUrl) {
|
||||
if (showDialog) ElMessage.error('下载链接不可用')
|
||||
return
|
||||
}
|
||||
|
||||
stage.value = 'downloading';
|
||||
prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' };
|
||||
stage.value = 'downloading'
|
||||
if (showDialog) show.value = true
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
|
||||
(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||||
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||||
prog.value = {
|
||||
percentage: progress.percentage || 0,
|
||||
current: progress.current || '0 MB',
|
||||
total: progress.total || '0 MB',
|
||||
speed: progress.speed || ''
|
||||
};
|
||||
});
|
||||
total: progress.total || '0 MB'
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await (window as any).electronAPI.downloadUpdate(info.value.downloadUrl)
|
||||
const response = await (window as any).electronAPI.downloadUpdate({
|
||||
asarUrl: info.value.asarUrl,
|
||||
jarUrl: info.value.jarUrl,
|
||||
latestVersion: info.value.latestVersion
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
stage.value = 'completed'
|
||||
prog.value.percentage = 100
|
||||
ElMessage({ message: '下载完成', type: 'success' })
|
||||
show.value = true
|
||||
ElMessage.success(showDialog ? '下载完成' : '更新已下载完成,可以安装了')
|
||||
} else {
|
||||
ElMessage({ message: '下载失败: ' + (response.error || '未知错误'), type: 'error' })
|
||||
stage.value = 'check'
|
||||
if (showDialog) ElMessage.error('下载失败: ' + (response.error || '未知错误'))
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage({ message: '下载失败', type: 'error' })
|
||||
stage.value = 'check'
|
||||
if (showDialog) ElMessage.error('下载失败')
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelDownload() {
|
||||
try {
|
||||
(window as any).electronAPI.removeDownloadProgressListener()
|
||||
await (window as any).electronAPI.cancelDownload()
|
||||
show.value = false
|
||||
stage.value = 'check'
|
||||
} catch (error) {
|
||||
console.error('取消下载失败:', error)
|
||||
show.value = false
|
||||
stage.value = 'check'
|
||||
}
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
await (window as any).electronAPI.cancelDownload().catch(() => {})
|
||||
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
show.value = false
|
||||
ElMessage.info('已取消下载')
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
|
||||
'确认安装',
|
||||
{
|
||||
confirmButtonText: '立即安装',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
await ElMessageBox.confirm('安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?', '确认安装', {
|
||||
confirmButtonText: '立即安装',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const response = await (window as any).electronAPI.installUpdate()
|
||||
|
||||
if (response.success) {
|
||||
ElMessage({ message: '应用即将重启', type: 'success' })
|
||||
ElMessage.success('应用即将重启')
|
||||
setTimeout(() => show.value = false, 1000)
|
||||
} else {
|
||||
ElMessage({ message: '重启失败: ' + (response.error || '未知错误'), type: 'error' })
|
||||
stage.value = 'check'
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('安装失败:', error)
|
||||
ElMessage({ message: '安装失败', type: 'error' })
|
||||
if (error !== 'cancel') ElMessage.error('安装失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDownloadedFiles() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要清除已下载的更新文件吗?清除后需要重新下载。', '确认清除', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const response = await (window as any).electronAPI.clearUpdateFiles()
|
||||
|
||||
if (response.success) {
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
show.value = false
|
||||
ElMessage.success('已清除下载文件')
|
||||
} else {
|
||||
ElMessage.error('清除失败: ' + (response.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error('清除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
version.value = await updateApi.getVersion()
|
||||
} catch (error) {
|
||||
console.error('获取版本失败:', error)
|
||||
version.value = await (window as any).electronAPI.getJarVersion()
|
||||
const pendingUpdate = await (window as any).electronAPI.checkPendingUpdate()
|
||||
|
||||
if (pendingUpdate?.hasPendingUpdate) {
|
||||
stage.value = 'completed'
|
||||
prog.value.percentage = 100
|
||||
return
|
||||
}
|
||||
|
||||
await checkUpdate(true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
(window as any).electronAPI.removeDownloadProgressListener()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.version-info {
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
background: rgba(255,255,255,0.9);
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:deep(.update-dialog .el-dialog) {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
:deep(.update-dialog .el-dialog__header) {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:deep(.update-dialog .el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.update-content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.update-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 1fr;
|
||||
align-items: start;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.left-pane { display: flex; flex-direction: column; align-items: flex-start; }
|
||||
.app-icon-large { width: 70px; height: 70px; border-radius: 12px; margin: 4px 0 0 0; }
|
||||
.right-pane { min-width: 0; }
|
||||
.right-pane .announce { font-size: 16px; font-weight: 600; color: #1f2937; margin: 4px 0 6px; word-break: break-word; }
|
||||
.right-pane .desc { font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; word-break: break-word; }
|
||||
|
||||
.update-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.update-header.text-center {
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 12px;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.update-header.text-center .app-icon {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.update-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.update-header p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.update-details {
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
margin: 12px 0 8px 0;
|
||||
}
|
||||
|
||||
.update-details.form { max-height: none; }
|
||||
.notes-box :deep(textarea.el-textarea__inner) { white-space: pre-wrap; }
|
||||
|
||||
.update-details h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.update-actions.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.update-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.update-actions.row .update-buttons { justify-content: space-between; }
|
||||
:deep(.update-actions.row .update-buttons .el-button) { flex: none; min-width: 100px; }
|
||||
.left-actions { display: flex; gap: 12px; }
|
||||
.right-actions { display: flex; gap: 8px; }
|
||||
|
||||
:deep(.update-buttons .el-button) {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.download-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.download-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.download-main {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.download-icon .app-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.download-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.download-info {
|
||||
margin-bottom: 12px;
|
||||
|
||||
}
|
||||
|
||||
.download-info p {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.download-progress {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.progress-details {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-details span {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__outer) {
|
||||
border-radius: 4px;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__inner) {
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.update-buttons .el-button--primary) {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.update-buttons .el-button--primary:hover) {
|
||||
background-color: #1d4ed8;
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
|
||||
:deep(.update-buttons .el-button:not(.el-button--primary)) {
|
||||
background-color: #f3f4f6;
|
||||
border-color: #d1d5db;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.update-buttons .el-button:not(.el-button--primary):hover) {
|
||||
background-color: #e5e7eb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
:deep(.update-dialog .el-dialog) {border-radius: 16px; box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);}
|
||||
/* 通用标题样式 */
|
||||
:deep(.update-dialog .el-dialog__title) {font-size: 14px; font-weight: 500; margin-left: 8px;}
|
||||
/* 默认标题样式(第一阶段 - 检查阶段) */
|
||||
:deep(.update-dialog.stage-check .el-dialog__header) {display: block; text-align: left;}
|
||||
/* 第二阶段 - 下载中,标题居中 */
|
||||
:deep(.update-dialog.stage-downloading .el-dialog__header) {display: block; text-align: center;}
|
||||
:deep(.update-dialog.stage-downloading .el-dialog__title) {margin-left: 20px;}
|
||||
/* 第三阶段 - 下载完成,标题居中 */
|
||||
:deep(.update-dialog.stage-completed .el-dialog__header) {display: block; text-align: center;}
|
||||
:deep(.update-dialog.stage-completed .el-dialog__title) {margin-left: 20px;}
|
||||
:deep(.update-dialog .el-dialog__body) {padding: 0;}
|
||||
.update-content {text-align: left;}
|
||||
.update-layout {display: grid; grid-template-columns: 88px 1fr; align-items: start; margin-bottom: 5px;}
|
||||
.left-pane {display: flex; flex-direction: column; align-items: flex-start;}
|
||||
.app-icon-large {width: 70px; height: 70px; border-radius: 12px; margin: 4px 0 0 0;}
|
||||
.right-pane {min-width: 0;}
|
||||
.right-pane .announce {font-size: 16px; font-weight: 600; color: #1f2937; margin: 4px 0 6px; word-break: break-word;}
|
||||
.right-pane .desc {font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; word-break: break-word;}
|
||||
.update-header {display: flex; align-items: flex-start; margin-bottom: 24px;}
|
||||
.update-header.text-center {text-align: center; flex-direction: column; align-items: center;}
|
||||
.app-icon {width: 70px; height: 70px; border-radius: 12px; margin-right: 16px; flex-shrink: 0;}
|
||||
.update-header.text-center .app-icon {margin-right: 0; margin-bottom: 16px;}
|
||||
.update-header h3 {font-size: 20px; font-weight: 600; margin: 16px 0 8px 0; color: #1f2937;}
|
||||
.update-header p {font-size: 14px; color: #6b7280; margin: 0; line-height: 1.5;}
|
||||
.update-details {border-radius: 8px; padding: 0; margin: 12px 0 8px 0;}
|
||||
.update-details.form {max-height: none;}
|
||||
.notes-box :deep(textarea.el-textarea__inner) {white-space: pre-wrap;}
|
||||
.update-details h4 {font-size: 14px; font-weight: 600; color: #374151; margin: 0 0 8px 0;}
|
||||
.update-actions.row {display: flex; flex-direction: column; align-items: stretch; gap: 12px;}
|
||||
.update-buttons {display: flex; justify-content: space-between; gap: 12px;}
|
||||
.update-actions.row .update-buttons {justify-content: space-between;}
|
||||
:deep(.update-actions.row .update-buttons .el-button) {flex: none; min-width: 100px;}
|
||||
.left-actions {display: flex; gap: 12px;}
|
||||
.right-actions {display: flex; gap: 8px;}
|
||||
:deep(.update-buttons .el-button) {flex: 1; height: 32px; font-size: 13px; border-radius: 8px;}
|
||||
.download-header h3 {font-size: 14px; font-weight: 500; margin: 0; color: #1f2937;}
|
||||
.download-main {display: grid; grid-template-columns: 80px 1fr; align-items: start;}
|
||||
.download-icon {display: flex; justify-content: center;}
|
||||
.download-icon .app-icon {width: 64px; height: 64px; border-radius: 12px;}
|
||||
.download-content {min-width: 0;}
|
||||
.download-info {margin-bottom: 12px;}
|
||||
.download-info p {font-size: 14px; font-weight: 600; color: #6b7280; margin: 0;}
|
||||
.download-progress {margin: 0;}
|
||||
.progress-info {display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 14px; color: #6b7280;}
|
||||
.progress-details {margin-top: 12px; display: flex; justify-content: space-between; align-items: center;}
|
||||
.progress-details span {font-size: 12px; color: #909399;}
|
||||
.action-buttons {display: flex; gap: 8px;}
|
||||
:deep(.el-progress-bar__outer) {border-radius: 4px; background-color: #e5e7eb;}
|
||||
:deep(.el-progress-bar__inner) {border-radius: 4px; transition: width 0.3s ease;}
|
||||
:deep(.update-buttons .el-button--primary) {background-color: #2563eb; border-color: #2563eb; font-weight: 500;}
|
||||
:deep(.update-buttons .el-button--primary:hover) {background-color: #1d4ed8; border-color: #1d4ed8;}
|
||||
:deep(.update-buttons .el-button:not(.el-button--primary)) {background-color: #f3f4f6; border-color: #d1d5db; color: #374151; font-weight: 500;}
|
||||
:deep(.update-buttons .el-button:not(.el-button--primary):hover) {background-color: #e5e7eb; border-color: #9ca3af;}
|
||||
</style>
|
||||
@@ -1,23 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, ArrowRight, Refresh, Monitor, Setting, User } from '@element-plus/icons-vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { Refresh, Setting, Minus, CloseBold } from '@element-plus/icons-vue'
|
||||
|
||||
interface Props {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
activeMenu: string
|
||||
isAuthenticated: boolean
|
||||
currentUsername: string
|
||||
currentVersion: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'go-back'): void
|
||||
(e: 'go-forward'): void
|
||||
(e: 'reload'): void
|
||||
(e: 'user-click'): void
|
||||
(e: 'logout'): void
|
||||
(e: 'open-device'): void
|
||||
(e: 'open-settings'): void
|
||||
(e: 'open-account-manager'): void
|
||||
(e: 'check-update'): void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const isMaximized = ref(false)
|
||||
|
||||
const displayUsername = computed(() => {
|
||||
return props.isAuthenticated ? props.currentUsername : '未登录'
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
{ command: 'check-update', label: computed(() => `检查更新 v${props.currentVersion}`), class: 'menu-item' },
|
||||
{ command: 'account-manager', label: '我的电商账号', class: 'menu-item' },
|
||||
{ command: 'device', label: '我的设备', class: 'menu-item' },
|
||||
{ command: 'settings', label: '设置', class: 'menu-item' },
|
||||
{ command: 'logout', label: '退出', class: 'menu-item logout-item', showIf: () => props.isAuthenticated }
|
||||
]
|
||||
|
||||
async function handleMinimize() {
|
||||
await (window as any).electronAPI.windowMinimize()
|
||||
}
|
||||
|
||||
async function handleMaximize() {
|
||||
await (window as any).electronAPI.windowMaximize()
|
||||
isMaximized.value = await (window as any).electronAPI.windowIsMaximized()
|
||||
}
|
||||
|
||||
async function handleClose() {
|
||||
await (window as any).electronAPI.windowClose()
|
||||
}
|
||||
|
||||
const commandMap: Record<string, keyof Emits> = {
|
||||
logout: 'logout',
|
||||
device: 'open-device',
|
||||
settings: 'open-settings',
|
||||
'account-manager': 'open-account-manager',
|
||||
'check-update': 'check-update'
|
||||
}
|
||||
|
||||
function handleCommand(command: string) {
|
||||
const emitName = commandMap[command]
|
||||
if (emitName) emit(emitName as any)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isMaximized.value = await (window as any).electronAPI.windowIsMaximized()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,149 +75,109 @@ defineEmits<Emits>()
|
||||
<div class="navbar-left">
|
||||
<div class="nav-controls">
|
||||
<button class="nav-btn" title="后退" @click="$emit('go-back')" :disabled="!canGoBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
<svg viewBox="0 0 24 24" class="arrow-icon">
|
||||
<path d="M15 18l-6-6 6-6" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" title="前进" @click="$emit('go-forward')" :disabled="!canGoForward">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
<svg viewBox="0 0 24 24" class="arrow-icon">
|
||||
<path d="M9 18l6-6-6-6" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="nav-btn-round" title="刷新" @click="$emit('reload')">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</button>
|
||||
|
||||
<!-- 设置下拉菜单 -->
|
||||
<el-dropdown trigger="click" @command="handleCommand" placement="bottom-end">
|
||||
<button class="nav-btn-round" title="设置">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="settings-dropdown">
|
||||
<el-dropdown-item disabled class="username-item">
|
||||
{{ displayUsername }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.command"
|
||||
v-show="!item.showIf || item.showIf()"
|
||||
:command="item.command"
|
||||
:class="item.class">
|
||||
{{ typeof item.label === 'string' ? item.label : item.label.value }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
</div>
|
||||
<div class="navbar-center">
|
||||
<div class="breadcrumbs">
|
||||
<span>首页</span>
|
||||
<span class="separator">></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="设置" @click="$emit('open-settings')">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</button>
|
||||
<button class="nav-btn-round" title="用户" @click="$emit('user-click')">
|
||||
<el-icon><User /></el-icon>
|
||||
</button>
|
||||
<!-- 窗口控制按钮 -->
|
||||
<div class="window-controls">
|
||||
<button class="window-btn window-btn-minimize" title="最小化" @click="handleMinimize">
|
||||
<el-icon><Minus /></el-icon>
|
||||
</button>
|
||||
<button class="window-btn window-btn-maximize" title="最大化" @click="handleMaximize">
|
||||
<svg viewBox="0 0 12 12" class="maximize-icon">
|
||||
<rect x="2" y="2" width="8" height="8" stroke="currentColor" fill="none" stroke-width="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="window-btn window-btn-close" title="关闭" @click="handleClose">
|
||||
<el-icon><CloseBold /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.top-navbar {
|
||||
height: 40px;
|
||||
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: 32px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
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:hover:not(:disabled) {
|
||||
background: #f5f7fa;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.top-navbar {height: 40px; 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); -webkit-app-region: drag; user-select: none;}
|
||||
.navbar-left {display: flex; align-items: center; gap: 8px; flex: 0 0 auto; -webkit-app-region: no-drag;}
|
||||
.navbar-center {display: flex; justify-content: center; flex: 1;}
|
||||
.navbar-right {display: flex; align-items: center; gap: 8px; flex: 0 0 auto; -webkit-app-region: no-drag;}
|
||||
.nav-controls {display: flex; gap: 4px;}
|
||||
.nav-btn {width: 28px; height: 28px; border: none; background: transparent; cursor: pointer; font-size: 16px; color: #606266; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; outline: none; border-radius: 4px;}
|
||||
.arrow-icon {width: 18px; height: 18px; flex-shrink: 0;}
|
||||
.arrow-icon path {stroke: currentColor;}
|
||||
.nav-btn:hover:not(:disabled) {background: rgba(0, 0, 0, 0.05); color: #409EFF;}
|
||||
.nav-btn:hover:not(:disabled) .arrow-icon path {stroke: #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: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
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:active {outline: none; border: none;}
|
||||
.nav-btn:disabled {cursor: not-allowed; background: transparent; color: #d0d0d0;}
|
||||
.nav-btn:disabled .arrow-icon path {stroke: #d0d0d0;}
|
||||
.nav-btn-round {width: 28px; height: 28px; border: none; border-radius: 4px; background: transparent; cursor: pointer; font-size: 16px; color: #606266; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; outline: none;}
|
||||
.nav-btn-round:hover {background: rgba(0, 0, 0, 0.05); color: #409EFF;}
|
||||
.nav-btn-round:focus,
|
||||
.nav-btn-round:active {
|
||||
outline: none;
|
||||
}
|
||||
.nav-btn-round:active {outline: none;}
|
||||
.breadcrumbs {display: flex; align-items: center; color: #606266; font-size: 14px;}
|
||||
.separator {margin: 0 6px; color: #c0c4cc; font-size: 14px;}
|
||||
/* 窗口控制按钮 */
|
||||
.window-controls {display: flex; align-items: center; gap: 4px; margin-left: 8px;}
|
||||
.window-btn {width: 32px; height: 28px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #606266; transition: all 0.2s ease; outline: none; padding: 0; margin: 0; border-radius: 4px;}
|
||||
.window-btn:hover {background: rgba(0, 0, 0, 0.05);}
|
||||
.window-btn:active {background: rgba(0, 0, 0, 0.1);}
|
||||
.window-btn-close:hover {background: #e81123; color: #ffffff;}
|
||||
.window-btn-close:active {background: #f1707a;}
|
||||
.maximize-icon {width: 12px; height: 12px;}
|
||||
/* 登录/注册按钮 */
|
||||
</style>
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 8px;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
<style>
|
||||
/* 设置下拉菜单样式 */
|
||||
.settings-dropdown {min-width: 180px !important; padding: 4px 0 !important; border-radius: 12px !important; margin-top: 4px !important;}
|
||||
.settings-dropdown .username-item {font-weight: 600 !important; color: #000000 !important; cursor: default !important; padding: 8px 16px !important; font-size: 14px !important;}
|
||||
.settings-dropdown .menu-item {padding: 8px 16px !important; font-size: 13px !important; color: #000000 !important; transition: all 0.2s ease !important;}
|
||||
.settings-dropdown .menu-item:hover {background: #f5f7fa !important; color: #409EFF !important;}
|
||||
.settings-dropdown .logout-item {color: #000000 !important;}
|
||||
.settings-dropdown .logout-item:hover {background: #f5f7fa !important; color: #409EFF !important;}
|
||||
.settings-dropdown .el-dropdown-menu__item.is-disabled {cursor: default !important; opacity: 1 !important;}
|
||||
</style>
|
||||
@@ -1,9 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted} from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {ref, computed, onMounted, defineAsyncComponent, inject} from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {rakutenApi} from '../../api/rakuten'
|
||||
import { batchConvertImages } from '../../utils/imageProxy'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
import { useFileDrop } from '../../composables/useFileDrop'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
|
||||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||
|
||||
// 接收VIP状态
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
}>()
|
||||
|
||||
// UI 与加载状态
|
||||
const loading = ref(false)
|
||||
@@ -11,12 +22,12 @@ const tableLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusType = ref<'info' | 'success' | 'warning' | 'error'>('info')
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
// 查询与上传
|
||||
const singleShopName = ref('')
|
||||
const currentBatchId = ref('')
|
||||
const uploadInputRef = ref<HTMLInputElement | null>(null)
|
||||
const dragActive = ref(false)
|
||||
|
||||
// 数据与分页
|
||||
const allProducts = ref<any[]>([])
|
||||
@@ -44,14 +55,18 @@ const activeStep = computed(() => {
|
||||
return 2
|
||||
})
|
||||
|
||||
// 试用期过期弹框
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
||||
|
||||
const vipStatus = inject<any>('vipStatus')
|
||||
|
||||
// 左侧:上传文件名与地区
|
||||
const selectedFileName = ref('')
|
||||
const pendingFile = ref<File | null>(null)
|
||||
const region = ref('JP')
|
||||
const regionOptions = [
|
||||
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
|
||||
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
|
||||
{ label: '中国 (China)', value: 'CN', flag: '🇨🇳' },
|
||||
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' }
|
||||
]
|
||||
// 获取数据筛选:查询日期
|
||||
const dateRange = ref<string[] | null>(null)
|
||||
@@ -116,17 +131,34 @@ function needsSearch(product: any) {
|
||||
}
|
||||
|
||||
async function loadLatest() {
|
||||
const resp = await rakutenApi.getLatestProducts()
|
||||
allProducts.value = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
const resp: any = await rakutenApi.getLatestProducts()
|
||||
const products = resp.data.products || []
|
||||
allProducts.value = products.map((p: any) => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
}
|
||||
|
||||
function hasValid1688Data(data: any) {
|
||||
if (!data) return false
|
||||
const skuJson = data.skuPriceJson || data.skuPrice
|
||||
const prices = parseSkuPrices({ skuPriceJson: skuJson })
|
||||
if (!data.mapRecognitionLink) return false
|
||||
if (!Array.isArray(prices) || !prices.length) return false
|
||||
if (!data.freight || data.freight <= 0) return false
|
||||
if (!data.median || data.median <= 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
async function searchProductInternal(product: any) {
|
||||
if (!product || !product.imgUrl) return
|
||||
if (!needsSearch(product)) return
|
||||
const res = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
|
||||
const data = res
|
||||
const skuJson = (data as any)?.skuPriceJson ?? (data as any)?.skuPrice
|
||||
if (!product || !product.imgUrl) return false
|
||||
if (!needsSearch(product)) return true
|
||||
if (!props.isVip) {
|
||||
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
|
||||
showTrialExpiredDialog.value = true
|
||||
return false
|
||||
}
|
||||
const res: any = await rakutenApi.search1688(product.imgUrl, currentBatchId.value, abortController?.signal)
|
||||
const data = res.data
|
||||
if (!hasValid1688Data(data)) return false
|
||||
const skuJson = data.skuPriceJson || data.skuPrice
|
||||
Object.assign(product, {
|
||||
mapRecognitionLink: data.mapRecognitionLink,
|
||||
freight: data.freight,
|
||||
@@ -138,6 +170,21 @@ async function searchProductInternal(product: any) {
|
||||
image1688Url: data.mapRecognitionLink,
|
||||
detailUrl1688: data.mapRecognitionLink,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function searchProductWithRetry(product: any, maxRetry = 2) {
|
||||
for (let attempt = 1; attempt <= maxRetry; attempt++) {
|
||||
try {
|
||||
const ok = await searchProductInternal(product)
|
||||
if (ok) return true
|
||||
} catch (e: any) {
|
||||
if (e.name === 'AbortError') return false
|
||||
console.warn('search1688 failed', e)
|
||||
}
|
||||
if (attempt < maxRetry) await delay(600)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function beforeUpload(file: File) {
|
||||
@@ -177,19 +224,28 @@ async function handleExcelUpload(e: Event) {
|
||||
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)
|
||||
}
|
||||
// 拖拽上传
|
||||
const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({
|
||||
accept: /\.xlsx?$/i,
|
||||
onFile: processFile,
|
||||
onError: (msg) => ElMessage({ message: msg, type: 'warning' })
|
||||
})
|
||||
|
||||
|
||||
// 点击“获取数据
|
||||
// 点击"获取数据
|
||||
async function handleStartSearch() {
|
||||
// 刷新VIP状态
|
||||
if (refreshVipStatus) await refreshVipStatus()
|
||||
|
||||
// VIP检查
|
||||
if (!props.isVip) {
|
||||
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
abortController = new AbortController()
|
||||
|
||||
if (pendingFile.value) {
|
||||
try {
|
||||
loading.value = true
|
||||
@@ -201,18 +257,19 @@ async function handleStartSearch() {
|
||||
progressPercentage.value = 0
|
||||
totalProducts.value = 0
|
||||
processedProducts.value = 0
|
||||
const resp = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value})
|
||||
const products = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
const resp: any = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value}, abortController?.signal)
|
||||
const products = (resp.data.products || []).map((p: any) => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
|
||||
if (products.length === 0) {
|
||||
showMessage('未采集到数据,请检查代理或店铺是否存在', 'warning')
|
||||
}
|
||||
|
||||
allProducts.value = products
|
||||
pendingFile.value = null
|
||||
} catch (e) {
|
||||
statusType.value = 'error'
|
||||
statusMessage.value = '解析失败,请重试'
|
||||
} catch (e: any) {
|
||||
if (e.name !== 'AbortError') {
|
||||
statusType.value = 'error'
|
||||
statusMessage.value = '解析失败,请重试'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
@@ -226,17 +283,21 @@ async function handleStartSearch() {
|
||||
progressPercentage.value = 100
|
||||
statusType.value = 'success'
|
||||
statusMessage.value = ''
|
||||
abortController = null
|
||||
return
|
||||
}
|
||||
if (items.length === 0) {
|
||||
statusType.value = 'warning'
|
||||
statusMessage.value = '没有可处理的商品,请先导入或查询店铺'
|
||||
abortController = null
|
||||
return
|
||||
}
|
||||
await startBatch1688Search(items)
|
||||
}
|
||||
|
||||
function stopTask() {
|
||||
abortController?.abort()
|
||||
abortController = null
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
statusType.value = 'warning'
|
||||
@@ -251,6 +312,7 @@ async function startBatch1688Search(products: any[]) {
|
||||
progressPercentage.value = 100
|
||||
statusType.value = 'success'
|
||||
statusMessage.value = '所有商品都已获取1688数据!'
|
||||
abortController = null
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
@@ -269,6 +331,7 @@ async function startBatch1688Search(products: any[]) {
|
||||
statusMessage.value = ''
|
||||
}
|
||||
loading.value = false
|
||||
abortController = null
|
||||
}
|
||||
|
||||
async function serialSearch1688(products: any[]) {
|
||||
@@ -276,7 +339,7 @@ async function serialSearch1688(products: any[]) {
|
||||
const product = products[i]
|
||||
product.searching1688 = true
|
||||
await nextTickSafe()
|
||||
await searchProductInternal(product)
|
||||
await searchProductWithRetry(product)
|
||||
product.searching1688 = false
|
||||
processedProducts.value++
|
||||
progressPercentage.value = Math.floor((processedProducts.value / Math.max(1, totalProducts.value)) * 100)
|
||||
@@ -292,7 +355,6 @@ function delay(ms: number) {
|
||||
}
|
||||
|
||||
function nextTickSafe() {
|
||||
// 不额外引入 nextTick,使用微任务刷新即可,保持体积精简
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -301,6 +363,14 @@ function showMessage(message: string, type: 'info' | 'success' | 'warning' | 'er
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
function removeSelectedFile() {
|
||||
selectedFileName.value = ''
|
||||
pendingFile.value = null
|
||||
if (uploadInputRef.value) {
|
||||
uploadInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
if (!allProducts.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
@@ -358,12 +428,10 @@ async function exportToExcel() {
|
||||
base64: base64Data,
|
||||
extension: 'jpeg',
|
||||
})
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: 1, row: row.number - 1 },
|
||||
ext: { width: 60, height: 60 }
|
||||
})
|
||||
|
||||
row.height = 50
|
||||
}
|
||||
}
|
||||
@@ -374,10 +442,11 @@ async function exportToExcel() {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const fileName = `乐天商品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
|
||||
await handlePlatformFileExport('rakuten', blob, fileName)
|
||||
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
const username = getUsernameFromToken()
|
||||
const success = await handlePlatformFileExport('rakuten', blob, fileName, username)
|
||||
if (success) {
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('导出失败', 'error')
|
||||
} finally {
|
||||
@@ -390,7 +459,6 @@ onMounted(loadLatest)
|
||||
</script>
|
||||
<template>
|
||||
<div class="rakuten-root">
|
||||
|
||||
<div class="main-container">
|
||||
<div class="body-layout">
|
||||
<!-- 左侧步骤栏 -->
|
||||
@@ -412,7 +480,7 @@ onMounted(loadLatest)
|
||||
<a class="link" @click.prevent="downloadRakutenTemplate">点击下载模板</a>
|
||||
</div>
|
||||
|
||||
<div class="dropzone" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload" :class="{ disabled: loading }">
|
||||
<div class="dropzone" :class="{ disabled: loading, active: dragActive }" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload">
|
||||
<div class="dz-el-icon">📤</div>
|
||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
||||
@@ -421,6 +489,7 @@ onMounted(loadLatest)
|
||||
<div v-if="selectedFileName" class="file-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ selectedFileName }}</span>
|
||||
<span class="delete-btn" @click="removeSelectedFile" title="删除文件">🗑️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,8 +500,8 @@ onMounted(loadLatest)
|
||||
<div class="step-header">
|
||||
<div class="title">网站地区</div>
|
||||
</div>
|
||||
<div class="desc">请选择目标网站地区,如:日本区。</div>
|
||||
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
|
||||
<div class="desc">仅支持乐天市场日本区商品查询,后续将开放更多乐天网站地区,敬请期待。</div>
|
||||
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%" disabled>
|
||||
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}
|
||||
</el-option>
|
||||
@@ -464,12 +533,8 @@ onMounted(loadLatest)
|
||||
</div>
|
||||
<div class="desc">点击下方按钮导出所有商品数据到 Excel 文件</div>
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="!allProducts.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
|
||||
<!-- 导出进度条 -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -483,6 +548,9 @@ onMounted(loadLatest)
|
||||
<el-button type="primary" class="btn-blue" @click="rakutenExampleVisible = false">我知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 试用期过期弹框 -->
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<div class="table-section">
|
||||
@@ -526,7 +594,7 @@ onMounted(loadLatest)
|
||||
</td>
|
||||
<td>
|
||||
<div class="image-container" v-if="row.imgUrl">
|
||||
<img :src="row.imgUrl" class="thumb" alt="thumb"/>
|
||||
<el-image :src="row.imgUrl" class="thumb" fit="contain" :preview-src-list="[row.imgUrl]" />
|
||||
</div>
|
||||
<span v-else>无图片</span>
|
||||
</td>
|
||||
@@ -562,9 +630,8 @@ onMounted(loadLatest)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination-fixed" >
|
||||
<div class="pagination-fixed">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
@@ -582,227 +649,101 @@ onMounted(loadLatest)
|
||||
</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;
|
||||
}
|
||||
|
||||
.body-layout { display: flex; gap: 12px; height: 100%; }
|
||||
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 8px; text-align: left; }
|
||||
|
||||
.rakuten-root {position: absolute; inset: 0; background: #fff; box-sizing: border-box;}
|
||||
.main-container {height: 100%; display: flex; flex-direction: column; padding: 12px; box-sizing: border-box;}
|
||||
.body-layout {display: flex; gap: 12px; height: 100%;}
|
||||
.steps-sidebar {width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0;}
|
||||
.steps-title {font-size: 14px; font-weight: 600; color: #303133; text-align: left;}
|
||||
/* 卡片式步骤,与示例一致 */
|
||||
.steps-flow { position: relative; }
|
||||
.steps-flow:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 10px; padding: 8px 0; }
|
||||
.flow-item + .flow-item { border-top: 1px dashed #ebeef5; }
|
||||
.flow-item .step-index { position: static; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.flow-item:after { display: none; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.title { font-size: 13px; font-weight: 600; color: #303133; text-align: left; }
|
||||
.desc { font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left; }
|
||||
.mini-hint { font-size: 12px; color: #909399; margin-top: 8px; text-align: left; }
|
||||
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
||||
.link { color: #409EFF; cursor: pointer; font-size: 12px; }
|
||||
.sep { color: #dcdfe6; }
|
||||
|
||||
.content-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
|
||||
.left-controls { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.dropzone { border: 1px dashed #c0c4cc; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; background: #fafafa; }
|
||||
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
||||
.dropzone.disabled { opacity: .6; cursor: not-allowed; }
|
||||
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
||||
.dz-text { color: #303133; font-size: 13px; }
|
||||
.dz-sub { color: #909399; font-size: 12px; }
|
||||
.single-input.left { display: flex; gap: 8px; }
|
||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; }
|
||||
.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; display: inline-block; }
|
||||
.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.progress-section.left { margin-top: 10px; }
|
||||
.full { width: 100%; }
|
||||
.form-row { margin-bottom: 10px; }
|
||||
.label { display: block; font-size: 12px; color: #606266; margin-bottom: 6px; }
|
||||
|
||||
.steps-flow {position: relative;}
|
||||
.steps-flow:before {content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6);}
|
||||
.flow-item {position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0;}
|
||||
.flow-item .step-index {position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px;}
|
||||
.flow-item:after {display: none;}
|
||||
.step-card {border: none; border-radius: 0; padding: 0; background: transparent; min-width: 0;}
|
||||
.step-header {display: flex; align-items: center; gap: 8px; margin-bottom: 6px;}
|
||||
.title {font-size: 13px; font-weight: 600; color: #303133; text-align: left;}
|
||||
.desc {font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left;}
|
||||
.mini-hint {font-size: 12px; color: #909399; margin-top: 8px; text-align: left;}
|
||||
.links {display: flex; align-items: center; gap: 6px; margin-bottom: 8px;}
|
||||
.link {color: #409EFF; cursor: pointer; font-size: 12px;}
|
||||
.sep {color: #dcdfe6;}
|
||||
.content-panel {flex: 1; display: flex; flex-direction: column; min-width: 0;}
|
||||
.left-controls {margin-top: 10px; display: flex; flex-direction: column; gap: 10px;}
|
||||
.dropzone {border: 1px dashed #c0c4cc; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; background: #fafafa;}
|
||||
.dropzone:hover {background: #f6fbff; border-color: #409EFF;}
|
||||
.dropzone.disabled {opacity: .6; cursor: not-allowed;}
|
||||
.dz-el-icon {font-size: 18px; margin-bottom: 4px; color: #909399;}
|
||||
.dz-text {color: #303133; font-size: 13px;}
|
||||
.dz-sub {color: #909399; font-size: 12px;}
|
||||
.single-input.left {display: flex; gap: 8px;}
|
||||
.action-buttons.column {display: flex; flex-direction: column; gap: 8px;}
|
||||
.file-chip {display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; width: 100%; box-sizing: border-box;}
|
||||
.file-chip .dot {width: 6px; height: 6px; background: #409EFF; border-radius: 50%; flex-shrink: 0;}
|
||||
.file-chip .name {flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
|
||||
.file-chip .delete-btn {cursor: pointer; opacity: 0.6; flex-shrink: 0;}
|
||||
.file-chip .delete-btn:hover {opacity: 1;}
|
||||
.progress-section.left {margin-top: 10px;}
|
||||
.full {width: 100%;}
|
||||
.form-row {margin-bottom: 10px;}
|
||||
.label {display: block; font-size: 12px; color: #606266; margin-bottom: 6px;}
|
||||
/* 统一左侧控件宽度与主色 */
|
||||
.steps-sidebar :deep(.el-date-editor),
|
||||
.steps-sidebar :deep(.el-range-editor.el-input__wrapper),
|
||||
.steps-sidebar :deep(.el-input),
|
||||
.steps-sidebar :deep(.el-input__wrapper),
|
||||
.steps-sidebar :deep(.el-select) { width: 100%; box-sizing: border-box; }
|
||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||||
.w100 { width: 100%; }
|
||||
.steps-sidebar :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.progress-section { margin: 0px 12px 0px 12px; }
|
||||
.progress-box { padding: 4px 0; }
|
||||
.progress-container { display: flex; align-items: center; gap: 8px; }
|
||||
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
||||
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
||||
|
||||
.current-status {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
padding-left: 2px;
|
||||
}
|
||||
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
|
||||
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
|
||||
.export-progress-fill { height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease; }
|
||||
.export-progress-text { font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right; }
|
||||
|
||||
.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; display: flex; flex-direction: column; }
|
||||
.table-wrapper { flex: 1; overflow: auto; }
|
||||
.table-wrapper { scrollbar-width: thin; scrollbar-color: #c0c4cc transparent; }
|
||||
.table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
||||
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
|
||||
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
|
||||
.table { width: max-content; min-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; }
|
||||
.empty-container { text-align: center; }
|
||||
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
|
||||
.empty-text { font-size: 14px; color: #909399; }
|
||||
.import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
|
||||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
||||
|
||||
.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;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
.steps-sidebar :deep(.el-select) {width: 100%; box-sizing: border-box;}
|
||||
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||
.w100 {width: 100%;}
|
||||
.steps-sidebar :deep(.el-button + .el-button) {margin-left: 0;}
|
||||
.progress-section {margin: 0px 12px 0px 12px;}
|
||||
.progress-box {padding: 4px 0;}
|
||||
.progress-container {display: flex; align-items: center; gap: 8px;}
|
||||
.progress-bar {flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden;}
|
||||
.progress-fill {height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease;}
|
||||
.progress-text {font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right;}
|
||||
.current-status {font-size: 12px; color: #606266; padding-left: 2px;}
|
||||
.export-progress {display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px;}
|
||||
.export-progress-bar {flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden;}
|
||||
.export-progress-fill {height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease;}
|
||||
.export-progress-text {font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right;}
|
||||
.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; display: flex; flex-direction: column;}
|
||||
.table-wrapper {flex: 1; overflow: auto;}
|
||||
.table-wrapper {scrollbar-width: thin; scrollbar-color: #c0c4cc transparent;}
|
||||
.table-wrapper::-webkit-scrollbar {width: 6px; height: 6px;}
|
||||
.table-wrapper::-webkit-scrollbar-track {background: transparent;}
|
||||
.table-wrapper::-webkit-scrollbar-thumb {background: #c0c4cc; border-radius: 3px;}
|
||||
.table-wrapper:hover::-webkit-scrollbar-thumb {background: #a8abb2;}
|
||||
.table {width: max-content; min-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;}
|
||||
.empty-container {text-align: center;}
|
||||
.empty-icon {font-size: 48px; margin-bottom: 12px; opacity: 0.6;}
|
||||
.empty-text {font-size: 14px; color: #909399;}
|
||||
.import-section.drag-active {border: 1px dashed #409EFF; border-radius: 6px;}
|
||||
.empty-abs {position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none;}
|
||||
.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; pointer-events: none;}
|
||||
.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 0 12px; background: #fff; display: flex; justify-content: flex-end;}
|
||||
.pagination-fixed :deep(.el-pager li.is-active) {border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff;}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, defineAsyncComponent, inject } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
|
||||
import AccountManager from '../common/AccountManager.vue'
|
||||
import { batchConvertImages } from '../../utils/imageProxy'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||
// 接收VIP状态
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
}>()
|
||||
type Shop = { id: string; shopName: string }
|
||||
|
||||
const accounts = ref<BanmaAccount[]>([])
|
||||
const accountId = ref<number>()
|
||||
// 收起功能移除
|
||||
|
||||
const shopList = ref<Shop[]>([])
|
||||
const selectedShops = ref<string[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
@@ -30,6 +33,12 @@ const fetchCurrentPage = ref(1)
|
||||
const fetchTotalPages = ref(0)
|
||||
const fetchTotalItems = ref(0)
|
||||
const isFetching = ref(false)
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
// 试用期过期弹框
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
|
||||
const vipStatus = inject<any>('vipStatus')
|
||||
function selectAccount(id: number) {
|
||||
accountId.value = id
|
||||
loadShops()
|
||||
@@ -63,7 +72,8 @@ async function loadShops() {
|
||||
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
const res = await zebraApi.getAccounts()
|
||||
const username = getUsernameFromToken()
|
||||
const res = await zebraApi.getAccounts(username)
|
||||
const list = (res as any)?.data ?? res
|
||||
accounts.value = Array.isArray(list) ? list : []
|
||||
const def = accounts.value.find(a => a.isDefault === 1) || accounts.value[0]
|
||||
@@ -86,6 +96,16 @@ function handleCurrentChange(page: number) {
|
||||
async function fetchData() {
|
||||
if (isFetching.value) return
|
||||
|
||||
// 刷新VIP状态
|
||||
if (refreshVipStatus) await refreshVipStatus()
|
||||
// VIP检查
|
||||
if (!props.isVip) {
|
||||
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
abortController = new AbortController()
|
||||
loading.value = true
|
||||
isFetching.value = true
|
||||
showProgress.value = true
|
||||
@@ -94,16 +114,17 @@ async function fetchData() {
|
||||
fetchCurrentPage.value = 1
|
||||
fetchTotalItems.value = 0
|
||||
currentBatchId.value = `ZEBRA_${Date.now()}`
|
||||
|
||||
const [startDate = '', endDate = ''] = dateRange.value || []
|
||||
|
||||
const [start, end] = dateRange.value || []
|
||||
const startDate = start ? `${new Date(start).toLocaleDateString('sv-SE')} 00:00:00` : ''
|
||||
const endDate = end ? `${new Date(end).toLocaleDateString('sv-SE')} 23:59:59` : ''
|
||||
|
||||
await fetchPageData(startDate, endDate)
|
||||
}
|
||||
|
||||
async function fetchPageData(startDate: string, endDate: string) {
|
||||
if (!isFetching.value) return
|
||||
|
||||
try {
|
||||
const data = await zebraApi.getOrders({
|
||||
const response = await zebraApi.getOrders({
|
||||
accountId: Number(accountId.value) || undefined,
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -111,14 +132,13 @@ async function fetchPageData(startDate: string, endDate: string) {
|
||||
pageSize: 50,
|
||||
shopIds: selectedShops.value.join(','),
|
||||
batchId: currentBatchId.value
|
||||
})
|
||||
}, abortController?.signal)
|
||||
|
||||
const data = (response as any)?.data || response
|
||||
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++
|
||||
@@ -127,8 +147,10 @@ async function fetchPageData(startDate: string, endDate: string) {
|
||||
progressPercentage.value = 100
|
||||
finishFetching()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取订单数据失败:', e)
|
||||
} catch (e: any) {
|
||||
if (e.name !== 'AbortError') {
|
||||
console.error('获取订单数据失败:', e)
|
||||
}
|
||||
finishFetching()
|
||||
}
|
||||
}
|
||||
@@ -136,6 +158,7 @@ async function fetchPageData(startDate: string, endDate: string) {
|
||||
function finishFetching() {
|
||||
isFetching.value = false
|
||||
loading.value = false
|
||||
abortController = null
|
||||
// 确保进度条完全填满
|
||||
progressPercentage.value = 100
|
||||
currentPage.value = 1
|
||||
@@ -143,6 +166,8 @@ function finishFetching() {
|
||||
}
|
||||
|
||||
function stopFetch() {
|
||||
abortController?.abort()
|
||||
abortController = null
|
||||
isFetching.value = false
|
||||
loading.value = false
|
||||
// 进度条保留显示,不自动隐藏
|
||||
@@ -237,9 +262,12 @@ async function exportToExcel() {
|
||||
})
|
||||
const fileName = `斑马订单数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
|
||||
await handlePlatformFileExport('zebra', blob, fileName)
|
||||
const username = getUsernameFromToken()
|
||||
const success = await handlePlatformFileExport('zebra', blob, fileName, username)
|
||||
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
if (success) {
|
||||
showMessage('Excel文件导出成功!', 'success')
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('导出失败', 'error')
|
||||
} finally {
|
||||
@@ -266,7 +294,19 @@ const rememberPwd = ref(true)
|
||||
const managerVisible = ref(false)
|
||||
const accountManagerRef = ref()
|
||||
|
||||
function openAddAccount() {
|
||||
async function openAddAccount() {
|
||||
try {
|
||||
const username = getUsernameFromToken()
|
||||
const limitRes = await zebraApi.getAccountLimit(username)
|
||||
const limitData = (limitRes as any)?.data ?? limitRes
|
||||
const { limit = 1, count = 0 } = limitData
|
||||
if (count >= limit) {
|
||||
ElMessage({ message: `账号数量已达上限(${limit}个),${limit < 3 ? '请升级订阅或' : ''}请先删除其他账号`, type: 'warning' })
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查账号限制失败:', e)
|
||||
}
|
||||
isEditMode.value = false
|
||||
accountForm.value = { name: '', username: '', isDefault: 0, status: 1 }
|
||||
formUsername.value = ''
|
||||
@@ -295,7 +335,8 @@ async function submitAccount() {
|
||||
status: accountForm.value.status || 1,
|
||||
}
|
||||
try {
|
||||
const res = await zebraApi.saveAccount(payload)
|
||||
const username = getUsernameFromToken()
|
||||
const res = await zebraApi.saveAccount(payload, username)
|
||||
const id = (res as any)?.data?.id || (res as any)?.id
|
||||
if (!id) throw new Error((res as any)?.msg || '保存失败')
|
||||
accountDialogVisible.value = false
|
||||
@@ -343,9 +384,8 @@ async function removeCurrentAccount() {
|
||||
>
|
||||
<span class="acct-row">
|
||||
<span :class="['status-dot', a.status === 1 ? 'on' : 'off']"></span>
|
||||
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" alt="avatar" />
|
||||
<img class="avatar" src="/image/user.png" alt="avatar" />
|
||||
<span class="acct-text">{{ a.name || a.username }}</span>
|
||||
<span v-if="a.isDefault===1" class="tag">默认</span>
|
||||
<span v-if="accountId === a.id" class="acct-check">✔️</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -368,8 +408,8 @@ async function removeCurrentAccount() {
|
||||
<section class="step">
|
||||
<div class="step-index">2</div>
|
||||
<div class="step-body">
|
||||
<div class="step-title">需要查询的日期</div>
|
||||
<div class="tip">请选择查询数据的日期范围。</div>
|
||||
<div class="step-title">需查询的店铺与日期</div>
|
||||
<div class="tip">请选择需查询的店铺(可多选)与日期范围,选项为空时默认获取全部数据</div>
|
||||
<el-select v-model="selectedShops" multiple placeholder="选择店铺" :disabled="loading || !accounts.length" size="small" style="width: 100%">
|
||||
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id" />
|
||||
</el-select>
|
||||
@@ -384,7 +424,7 @@ async function removeCurrentAccount() {
|
||||
<div class="step-title">获取数据</div>
|
||||
<div class="tip">点击下方按钮,开始查询订单数据。</div>
|
||||
<div class="btn-col">
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="loading || !accounts.length" @click="fetchData">{{ loading ? '处理中...' : '获取数据' }}</el-button>
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="loading || exportLoading || !accounts.length" @click="fetchData">{{ loading ? '处理中...' : '获取数据' }}</el-button>
|
||||
<el-button size="small" :disabled="!loading" @click="stopFetch" class="w100">停止获取</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,7 +436,7 @@ async function removeCurrentAccount() {
|
||||
<div class="step-title">导出数据</div>
|
||||
<div class="tip">点击下方按钮导出所有订单数据到 Excel 文件</div>
|
||||
<div class="btn-col">
|
||||
<el-button size="small" type="success" :disabled="exportLoading || !allOrderData.length" :loading="exportLoading" @click="exportToExcel" class="w100">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
|
||||
<el-button size="small" :disabled="exportLoading || loading || !allOrderData.length" :loading="exportLoading" @click="exportToExcel" class="w100 btn-blue">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
|
||||
<!-- 导出进度条 -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,7 +485,7 @@ async function removeCurrentAccount() {
|
||||
<td>{{ row.orderedAt || '-' }}</td>
|
||||
<td>
|
||||
<div class="image-container" v-if="row.productImage">
|
||||
<img :src="row.productImage" class="thumb" alt="thumb" />
|
||||
<el-image :src="row.productImage" class="thumb" fit="contain" :preview-src-list="[row.productImage]" />
|
||||
</div>
|
||||
<span v-else>无图片</span>
|
||||
</td>
|
||||
@@ -487,7 +527,6 @@ async function removeCurrentAccount() {
|
||||
<!-- 底部区域:分页器 -->
|
||||
<div class="pagination-fixed">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
@@ -521,7 +560,11 @@ async function removeCurrentAccount() {
|
||||
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">登录</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
|
||||
|
||||
<!-- 试用期过期弹框 -->
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
|
||||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @refresh="loadAccounts" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -532,93 +575,94 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.layout { background: #fff; border-radius: 4px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
|
||||
.aside { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease; }
|
||||
.aside.collapsed { width: 56px; overflow: hidden; }
|
||||
.aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
||||
.aside-steps { position: relative; }
|
||||
.step { display: grid; grid-template-columns: 24px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
||||
.step + .step { border-top: 1px dashed #ebeef5; }
|
||||
.step-index { width: 24px; height: 24px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.step-body { min-width: 0; text-align: left; }
|
||||
.step-title { font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left; }
|
||||
.aside-steps:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.account-list {height: auto; }
|
||||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.step-accounts { position: relative; }
|
||||
.sticky-actions { position: sticky; bottom: 0; background: #fafafa; padding-top: 8px; }
|
||||
.scroll-limit { max-height: 160px; }
|
||||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.btn-col { display: flex; flex-direction: column; gap: 6px; }
|
||||
.w50 { width: 48%; }
|
||||
.w100 { width: 100%; }
|
||||
.placeholder-box { display:flex; align-items:center; justify-content:center; flex-direction:column; height: 140px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
.placeholder-img { width: 120px; opacity: 0.9; }
|
||||
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
|
||||
.aside :deep(.el-date-editor) { width: 100%; }
|
||||
.aside :deep(.el-range-editor.el-input__wrapper) { width: 100%; box-sizing: border-box; }
|
||||
.zebra-root {position: absolute; inset: 0; background: #fff; box-sizing: border-box;}
|
||||
.layout {height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; padding: 12px; box-sizing: border-box;}
|
||||
.aside {border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease;}
|
||||
.aside.collapsed {width: 56px; overflow: hidden;}
|
||||
.aside-header {display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px;}
|
||||
.aside-steps {position: relative;}
|
||||
.step {display: grid; grid-template-columns: 22px 1fr; gap: 10px; position: relative; padding: 8px 0;}
|
||||
.step-index {width: 22px; height: 22px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px;}
|
||||
.step-body {min-width: 0; text-align: left;}
|
||||
.step-title {font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left;}
|
||||
.aside-steps:before {content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6);}
|
||||
.account-list {height: auto;}
|
||||
.step-actions {margin-top: 8px; display: flex; gap: 8px;}
|
||||
.step-accounts {position: relative;}
|
||||
.sticky-actions {position: sticky; bottom: 0; background: #fafafa; padding-top: 8px;}
|
||||
.scroll-limit {max-height: 160px;}
|
||||
.btn-row {display: grid; grid-template-columns: 1fr 1fr; gap: 8px;}
|
||||
.btn-col {display: flex; flex-direction: column; gap: 6px;}
|
||||
.w50 {width: 48%;}
|
||||
.w100 {width: 100%;}
|
||||
.placeholder-box {display:flex; align-items:center; justify-content:center; flex-direction:column; height: 140px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px;}
|
||||
.placeholder-img {width: 120px; opacity: 0.9;}
|
||||
.placeholder-tip {margin-top: 6px; font-size: 12px; color: #a8abb2;}
|
||||
.aside :deep(.el-date-editor) {width: 100%;}
|
||||
.aside :deep(.el-range-editor.el-input__wrapper) {width: 100%; box-sizing: border-box;}
|
||||
.aside :deep(.el-input),
|
||||
.aside :deep(.el-input__wrapper),
|
||||
.aside :deep(.el-select) { width: 100%; box-sizing: border-box; }
|
||||
.aside :deep(.el-button + .el-button) { margin-left: 0 !important; }
|
||||
.btn-row :deep(.el-button) { width: 100%; }
|
||||
.btn-col :deep(.el-button) { width: 100%; }
|
||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||||
.tip { color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left; }
|
||||
.avatar { width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; }
|
||||
.acct-text { vertical-align: middle; }
|
||||
.acct-row { display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
|
||||
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
|
||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
||||
.status-dot.on { background: #22c55e; }
|
||||
.status-dot.off { background: #f87171; }
|
||||
.acct-item { padding: 6px 8px; border-radius: 8px; cursor: pointer; }
|
||||
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
|
||||
.acct-check { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: transparent; color: #111; font-size: 14px; }
|
||||
.account-list::-webkit-scrollbar { width: 0; height: 0; }
|
||||
.add-account-dialog .aad-header { display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%; }
|
||||
.add-account-dialog .aad-icon { width: 120px; height: auto; }
|
||||
.add-account-dialog .aad-title { font-weight: 600; font-size: 18px; text-align: center; }
|
||||
.add-account-dialog .aad-row { margin-top: 12px; }
|
||||
.add-account-dialog .aad-opts { display:flex; align-items:center; }
|
||||
|
||||
.aside :deep(.el-select) {width: 100%; box-sizing: border-box;}
|
||||
.aside :deep(.el-button + .el-button) {margin-left: 0 !important;}
|
||||
.btn-row :deep(.el-button) {width: 100%;}
|
||||
.btn-col :deep(.el-button) {width: 100%;}
|
||||
.btn-blue {background: #1677FF; border-color: #1677FF; color: #fff;}
|
||||
.btn-blue:disabled {background: #a6c8ff; border-color: #a6c8ff; color: #fff;}
|
||||
.tip {color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left;}
|
||||
.avatar {width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px;}
|
||||
.acct-text {vertical-align: middle;}
|
||||
.acct-row {display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%;}
|
||||
.acct-text {overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px;}
|
||||
.status-dot {width: 6px; height: 6px; border-radius: 50%; display: inline-block;}
|
||||
.status-dot.on {background: #22c55e;}
|
||||
.status-dot.off {background: #f87171;}
|
||||
.acct-item {padding: 6px 8px; border-radius: 8px; cursor: pointer;}
|
||||
.acct-item.selected {background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff;}
|
||||
.acct-check {display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: transparent; color: #111; font-size: 14px;}
|
||||
.account-list::-webkit-scrollbar {width: 0; height: 0;}
|
||||
.add-account-dialog .aad-header {display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%;}
|
||||
.add-account-dialog .aad-icon {width: 120px; height: auto;}
|
||||
.add-account-dialog .aad-title {font-weight: 600; font-size: 18px; text-align: center;}
|
||||
.add-account-dialog .aad-row {margin-top: 12px;}
|
||||
.add-account-dialog .aad-opts {display:flex; align-items:center;}
|
||||
/* 居中 header,避免右上角关闭按钮影响视觉中心 */
|
||||
:deep(.add-account-dialog .el-dialog__header) { text-align: center; padding-right: 0; display: block; }
|
||||
.content { display: grid; grid-template-rows: 1fr auto; min-height: 0; }
|
||||
.table-section { min-height: 0; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
||||
.table-wrapper { flex: 1; overflow: auto; overflow-x: auto; }
|
||||
.table-wrapper { scrollbar-width: thin; scrollbar-color: #c0c4cc transparent; }
|
||||
.table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
||||
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
|
||||
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
|
||||
.table { width: max-content; min-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; 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: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.image-container { display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
|
||||
.thumb { width: 22px; height: 22px; 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 { position: sticky; bottom: 0; z-index: 2; 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: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
|
||||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
||||
.progress-section { margin: 0px 12px 0px 12px; }
|
||||
.progress-box { padding: 4px 0; }
|
||||
.progress-container { display: flex; align-items: center; gap: 8px; }
|
||||
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
||||
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
||||
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
|
||||
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
|
||||
.export-progress-fill { height: 100%; background: #67c23a; border-radius: 2px; transition: width 0.3s ease; }
|
||||
.export-progress-text { font-size: 11px; color: #67c23a; font-weight: 500; min-width: 32px; text-align: right; }
|
||||
:deep(.add-account-dialog .el-dialog__header) {text-align: center; padding-right: 0; display: block;}
|
||||
.content {display: grid; grid-template-rows: 1fr auto; min-height: 0;}
|
||||
.table-section {min-height: 0; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column;}
|
||||
.table-wrapper {flex: 1; overflow: auto; overflow-x: auto;}
|
||||
.table-wrapper {scrollbar-width: thin; scrollbar-color: #c0c4cc transparent;}
|
||||
.table-wrapper::-webkit-scrollbar {width: 6px; height: 6px;}
|
||||
.table-wrapper::-webkit-scrollbar-track {background: transparent;}
|
||||
.table-wrapper::-webkit-scrollbar-thumb {background: #c0c4cc; border-radius: 3px;}
|
||||
.table-wrapper:hover::-webkit-scrollbar-thumb {background: #a8abb2;}
|
||||
.table {width: max-content; min-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; 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: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
||||
.image-container {display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px;}
|
||||
.thumb {width: 22px; height: 22px; 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 {position: sticky; bottom: 0; z-index: 2; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end;}
|
||||
.pagination-fixed :deep(.el-pager li.is-active) {border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff;}
|
||||
.tag {display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px;}
|
||||
.empty-abs {position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none;}
|
||||
.progress-section {margin: 0px 12px 0px 12px;}
|
||||
.progress-box {padding: 4px 0;}
|
||||
.progress-container {display: flex; align-items: center; gap: 8px;}
|
||||
.progress-bar {flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden;}
|
||||
.progress-fill {height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease;}
|
||||
.progress-text {font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right;}
|
||||
.export-progress {display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px;}
|
||||
.export-progress-bar {flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden;}
|
||||
.export-progress-fill {height: 100%; background: #67c23a; border-radius: 2px; transition: width 0.3s ease;}
|
||||
.export-progress-text {font-size: 11px; color: #67c23a; font-weight: 500; min-width: 32px; text-align: right;}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface UseFileDropOptions {
|
||||
accept?: RegExp
|
||||
onFile: (file: File) => void | Promise<void>
|
||||
onError?: (message: string) => void
|
||||
}
|
||||
|
||||
export function useFileDrop(options: UseFileDropOptions) {
|
||||
const { accept, onFile, onError } = options
|
||||
const dragActive = ref(false)
|
||||
let dragCounter = 0
|
||||
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dragCounter++
|
||||
dragActive.value = true
|
||||
}
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dragCounter--
|
||||
if (dragCounter === 0) {
|
||||
dragActive.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = async (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dragCounter = 0
|
||||
dragActive.value = false
|
||||
|
||||
const file = e.dataTransfer?.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (accept && !accept.test(file.name)) {
|
||||
const fileTypes = accept.source.includes('xlsx?') ? '.xls 或 .xlsx' : accept.source
|
||||
onError?.(`仅支持 ${fileTypes} 文件`)
|
||||
return
|
||||
}
|
||||
|
||||
await onFile(file)
|
||||
}
|
||||
|
||||
return {
|
||||
dragActive,
|
||||
onDragEnter,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop
|
||||
}
|
||||
}
|
||||
|
||||
20
electron-vue-template/src/renderer/config/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 应用配置
|
||||
*/
|
||||
export const AppConfig = {
|
||||
CLIENT_BASE: 'http://localhost:8081',
|
||||
RUOYI_BASE: 'http://8.138.23.49:8085',
|
||||
get SSE_URL() {
|
||||
return `${this.RUOYI_BASE}/monitor/account/events`
|
||||
}
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 判断路径是否路由到ruoyi-admin服务
|
||||
*/
|
||||
export function isRuoyiPath(path: string): boolean {
|
||||
return path.startsWith('/monitor/') ||
|
||||
path.startsWith('/system/') ||
|
||||
path.startsWith('/tool/banma') ||
|
||||
path.startsWith('/tool/genmai')
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
<title>erpClient</title>
|
||||
<link rel="icon" href="/icon/icon.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<style>
|
||||
body { margin: 0; background-color: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<!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>
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export default interface ElectronApi {
|
||||
getUpdateStatus: () => Promise<{ downloadedFilePath: string | null; isDownloading: boolean; downloadProgress: any; isPackaged: boolean }>
|
||||
onUpdateLog: (callback: (log: string) => void) => void
|
||||
removeUpdateLogListener: () => void
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
6
electron-vue-template/src/renderer/typings/element-plus-shim.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module 'element-plus' {
|
||||
export const ElMessage: (options: { message: string; type?: 'success' | 'warning' | 'error' | 'info' }) => void
|
||||
export const ElMessageBox: { confirm: (message: string, title?: string, options?: any) => Promise<void> }
|
||||
}
|
||||
|
||||
|
||||
24
electron-vue-template/src/renderer/utils/deviceId.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CONFIG } from '../api/http'
|
||||
|
||||
const DEVICE_ID_KEY = 'device_id'
|
||||
|
||||
async function fetchDeviceIdFromClient(): Promise<string> {
|
||||
const response = await fetch(`${CONFIG.CLIENT_BASE}/api/system/device-id`, {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
cache: 'no-store'
|
||||
})
|
||||
if (!response.ok) throw new Error('获取设备ID失败')
|
||||
const result = await response.json()
|
||||
if (!result?.data) throw new Error('设备ID为空')
|
||||
return result.data
|
||||
}
|
||||
|
||||
export async function getOrCreateDeviceId(): Promise<string> {
|
||||
const cached = localStorage.getItem(DEVICE_ID_KEY)
|
||||
if (cached) return cached
|
||||
const deviceId = await fetchDeviceIdFromClient()
|
||||
localStorage.setItem(DEVICE_ID_KEY, deviceId)
|
||||
return deviceId
|
||||
}
|
||||
|
||||
177
electron-vue-template/src/renderer/utils/imageCompressor.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 图片压缩工具 - 在上传前压缩图片,减少传输和存储大小
|
||||
* 保持视觉效果的同时显著减小文件体积
|
||||
*/
|
||||
|
||||
export interface CompressOptions {
|
||||
/** 目标质量 0-1,默认0.8 */
|
||||
quality?: number
|
||||
/** 最大宽度,超过则等比缩放,默认1920 */
|
||||
maxWidth?: number
|
||||
/** 最大高度,超过则等比缩放,默认1080 */
|
||||
maxHeight?: number
|
||||
/** 输出格式,默认'image/jpeg' */
|
||||
mimeType?: string
|
||||
/** 是否转换为WebP格式(更小体积),默认false */
|
||||
useWebP?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片文件
|
||||
* @param file 原始图片文件
|
||||
* @param options 压缩选项
|
||||
* @returns 压缩后的Blob和压缩信息
|
||||
*/
|
||||
export async function compressImage(
|
||||
file: File,
|
||||
options: CompressOptions = {}
|
||||
): Promise<{
|
||||
blob: Blob
|
||||
file: File
|
||||
originalSize: number
|
||||
compressedSize: number
|
||||
compressionRatio: number
|
||||
}> {
|
||||
const {
|
||||
quality = 0.85,
|
||||
maxWidth = 1920,
|
||||
maxHeight = 1080,
|
||||
mimeType = 'image/jpeg',
|
||||
useWebP = false,
|
||||
} = options
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 计算压缩后的尺寸(保持宽高比)
|
||||
let { width, height } = img
|
||||
const aspectRatio = width / height
|
||||
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth
|
||||
height = width / aspectRatio
|
||||
}
|
||||
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
width = height * aspectRatio
|
||||
}
|
||||
|
||||
// 创建canvas进行压缩
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
reject(new Error('无法获取canvas上下文'))
|
||||
return
|
||||
}
|
||||
|
||||
// 绘制图片
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
// 转换为Blob - 如果原图是PNG,保持PNG格式以保留透明度
|
||||
const isPNG = file.type === 'image/png'
|
||||
const outputMimeType = useWebP ? 'image/webp' : (isPNG ? 'image/png' : mimeType)
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error('图片压缩失败'))
|
||||
return
|
||||
}
|
||||
|
||||
const originalSize = file.size
|
||||
const compressedSize = blob.size
|
||||
const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2)
|
||||
|
||||
// 转换为File对象
|
||||
const isPNG = file.type === 'image/png'
|
||||
let newFileName = file.name
|
||||
if (useWebP) {
|
||||
newFileName = file.name.replace(/\.[^.]+$/, '.webp')
|
||||
} else if (!isPNG) {
|
||||
newFileName = file.name.replace(/\.[^.]+$/, '.jpg')
|
||||
}
|
||||
// 如果是PNG,保持原文件名
|
||||
|
||||
const compressedFile = new File(
|
||||
[blob],
|
||||
newFileName,
|
||||
{ type: outputMimeType }
|
||||
)
|
||||
|
||||
resolve({
|
||||
blob,
|
||||
file: compressedFile,
|
||||
originalSize,
|
||||
compressedSize,
|
||||
compressionRatio: parseFloat(compressionRatio),
|
||||
})
|
||||
},
|
||||
outputMimeType,
|
||||
isPNG ? 1.0 : quality // PNG使用无损压缩(quality=1.0)
|
||||
)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('图片加载失败'))
|
||||
}
|
||||
|
||||
img.src = e.target?.result as string
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('文件读取失败'))
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 预设压缩配置
|
||||
*/
|
||||
export const COMPRESS_PRESETS = {
|
||||
/** 高质量(适合开屏图片、品牌Logo)- 1920x1080, 85%质量 */
|
||||
HIGH: {
|
||||
quality: 0.85,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
/** 中等质量(适合一般图片)- 1280x720, 80%质量 */
|
||||
MEDIUM: {
|
||||
quality: 0.8,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
/** 缩略图(适合列表展示)- 400x400, 75%质量 */
|
||||
THUMBNAIL: {
|
||||
quality: 0.75,
|
||||
maxWidth: 400,
|
||||
maxHeight: 400,
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
} as const
|
||||
78
electron-vue-template/src/renderer/utils/imageProxy.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
/**
|
||||
* 通过后端代理获取图片并转换为Base64
|
||||
* @param imageUrl 原始图片URL
|
||||
* @param maxSize 最大尺寸,默认80px
|
||||
* @returns Promise<string | null> Base64字符串或null
|
||||
*/
|
||||
export async function convertImageToBase64ViaProxy(imageUrl: string, maxSize: number = 80): Promise<string | null> {
|
||||
if (!imageUrl) return null
|
||||
try {
|
||||
const proxyUrl = `http://127.0.0.1:8081/api/system/proxy/image?url=${encodeURIComponent(imageUrl)}`
|
||||
const response = await fetch(proxyUrl)
|
||||
if (!response.ok) return null
|
||||
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
if (!arrayBuffer || arrayBuffer.byteLength === 0) return null
|
||||
if (arrayBuffer.byteLength < 1000) return null
|
||||
|
||||
const mimeType = contentType && contentType.startsWith('image/') ? contentType : 'image/jpeg'
|
||||
const imageBlob = new Blob([arrayBuffer], { type: mimeType })
|
||||
const objectUrl = URL.createObjectURL(imageBlob)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ratio = Math.min(maxSize / img.width, maxSize / img.height)
|
||||
canvas.width = img.width * ratio
|
||||
canvas.height = img.height * ratio
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
const base64 = canvas.toDataURL('image/jpeg', 0.8)
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
resolve(base64)
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
img.src = objectUrl
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量处理图片转换
|
||||
* @param imageUrls 图片URL数组
|
||||
* @param maxSize 最大尺寸
|
||||
* @returns Promise<(string | null)[]> Base64数组
|
||||
*/
|
||||
export async function batchConvertImages(imageUrls: string[], maxSize: number = 80): Promise<(string | null)[]> {
|
||||
const promises = imageUrls.map(async (url) => {
|
||||
if (!url) return null
|
||||
return await convertImageToBase64ViaProxy(url, maxSize)
|
||||
})
|
||||
|
||||
return await Promise.all(promises)
|
||||
}
|
||||
152
electron-vue-template/src/renderer/utils/settings.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// 应用设置管理工具
|
||||
|
||||
export type Platform = 'amazon' | 'rakuten' | 'zebra'
|
||||
|
||||
export interface PlatformExportSettings {
|
||||
exportPath: string
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
// 平台特定设置
|
||||
platforms: {
|
||||
amazon: PlatformExportSettings
|
||||
rakuten: PlatformExportSettings
|
||||
zebra: PlatformExportSettings
|
||||
}
|
||||
// 更新设置
|
||||
autoUpdate?: boolean
|
||||
// 关闭行为
|
||||
closeAction?: 'quit' | 'minimize' | 'tray'
|
||||
// 启动配置
|
||||
autoLaunch?: boolean
|
||||
launchMinimized?: boolean
|
||||
}
|
||||
|
||||
const SETTINGS_KEY_PREFIX = 'app-settings'
|
||||
|
||||
// 获取带用户隔离的设置 key
|
||||
function getSettingsKey(username?: string): string {
|
||||
if (!username || username.trim() === '') {
|
||||
return SETTINGS_KEY_PREFIX
|
||||
}
|
||||
return `${SETTINGS_KEY_PREFIX}-${username}`
|
||||
}
|
||||
|
||||
// 默认平台设置
|
||||
const defaultPlatformSettings: PlatformExportSettings = {
|
||||
exportPath: ''
|
||||
}
|
||||
|
||||
// 默认设置
|
||||
const defaultSettings: AppSettings = {
|
||||
platforms: {
|
||||
amazon: { ...defaultPlatformSettings },
|
||||
rakuten: { ...defaultPlatformSettings },
|
||||
zebra: { ...defaultPlatformSettings }
|
||||
},
|
||||
autoUpdate: false,
|
||||
closeAction: 'quit',
|
||||
autoLaunch: false,
|
||||
launchMinimized: false
|
||||
}
|
||||
|
||||
// 获取设置(按用户隔离)
|
||||
export function getSettings(username?: string): AppSettings {
|
||||
const settingsKey = getSettingsKey(username)
|
||||
const saved = localStorage.getItem(settingsKey)
|
||||
if (saved) {
|
||||
const settings = JSON.parse(saved)
|
||||
return {
|
||||
platforms: {
|
||||
amazon: { ...defaultSettings.platforms.amazon, ...settings.platforms?.amazon },
|
||||
rakuten: { ...defaultSettings.platforms.rakuten, ...settings.platforms?.rakuten },
|
||||
zebra: { ...defaultSettings.platforms.zebra, ...settings.platforms?.zebra }
|
||||
},
|
||||
autoUpdate: settings.autoUpdate ?? defaultSettings.autoUpdate,
|
||||
closeAction: settings.closeAction ?? defaultSettings.closeAction,
|
||||
autoLaunch: settings.autoLaunch ?? defaultSettings.autoLaunch,
|
||||
launchMinimized: settings.launchMinimized ?? defaultSettings.launchMinimized
|
||||
}
|
||||
}
|
||||
return defaultSettings
|
||||
}
|
||||
|
||||
// 保存设置(按用户隔离)
|
||||
export function saveSettings(settings: Partial<AppSettings>, username?: string): void {
|
||||
const current = getSettings(username)
|
||||
const updated = {
|
||||
platforms: {
|
||||
amazon: { ...current.platforms.amazon, ...settings.platforms?.amazon },
|
||||
rakuten: { ...current.platforms.rakuten, ...settings.platforms?.rakuten },
|
||||
zebra: { ...current.platforms.zebra, ...settings.platforms?.zebra }
|
||||
},
|
||||
autoUpdate: settings.autoUpdate ?? current.autoUpdate,
|
||||
closeAction: settings.closeAction ?? current.closeAction,
|
||||
autoLaunch: settings.autoLaunch ?? current.autoLaunch,
|
||||
launchMinimized: settings.launchMinimized ?? current.launchMinimized
|
||||
}
|
||||
const settingsKey = getSettingsKey(username)
|
||||
localStorage.setItem(settingsKey, JSON.stringify(updated))
|
||||
}
|
||||
|
||||
// 保存平台特定设置(按用户隔离)
|
||||
export function savePlatformSettings(platform: Platform, settings: Partial<PlatformExportSettings>, username?: string): void {
|
||||
const current = getSettings(username)
|
||||
const updated = {
|
||||
...current,
|
||||
platforms: {
|
||||
...current.platforms,
|
||||
[platform]: { ...current.platforms[platform], ...settings }
|
||||
}
|
||||
}
|
||||
const settingsKey = getSettingsKey(username)
|
||||
localStorage.setItem(settingsKey, JSON.stringify(updated))
|
||||
}
|
||||
|
||||
// 获取平台导出配置(按用户隔离)
|
||||
export function getPlatformExportConfig(platform: Platform, username?: string): PlatformExportSettings {
|
||||
const settings = getSettings(username)
|
||||
return settings.platforms[platform]
|
||||
}
|
||||
|
||||
|
||||
// 处理平台特定文件导出(按用户隔离)
|
||||
export async function handlePlatformFileExport(
|
||||
platform: Platform,
|
||||
blob: Blob,
|
||||
defaultFileName: string,
|
||||
username?: string
|
||||
): Promise<boolean> {
|
||||
const config = getPlatformExportConfig(platform, username)
|
||||
|
||||
if (!config.exportPath) {
|
||||
const result = await (window as any).electronAPI.showSaveDialog({
|
||||
title: '保存文件',
|
||||
defaultPath: defaultFileName,
|
||||
filters: [
|
||||
{ name: 'Excel 文件', extensions: ['xlsx', 'xls'] },
|
||||
{ name: '所有文件', extensions: ['*'] }
|
||||
]
|
||||
})
|
||||
|
||||
if (!result.canceled && result.filePath) {
|
||||
await writeFileToPath(blob, result.filePath)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
const filePath = `${config.exportPath}/${defaultFileName}`
|
||||
await writeFileToPath(blob, filePath)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 写入文件到指定路径
|
||||
async function writeFileToPath(blob: Blob, filePath: string): Promise<void> {
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const buffer = new Uint8Array(arrayBuffer)
|
||||
const result = await (window as any).electronAPI.writeFile(filePath, buffer)
|
||||
if (!result.success) throw new Error(result.error)
|
||||
}
|
||||
|
||||
49
electron-vue-template/src/renderer/utils/token.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// Token 工具函数
|
||||
export const TOKEN_KEY = 'auth_token';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function removeToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getUsernameFromToken(token?: string): string {
|
||||
try {
|
||||
const t = token || getToken();
|
||||
if (!t) return '';
|
||||
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||
return payload.username || payload.sub || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getClientIdFromToken(token?: string): string {
|
||||
try {
|
||||
const t = token || getToken();
|
||||
if (!t) return '';
|
||||
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||
return payload.clientId || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getRegisterTimeFromToken(token?: string): string {
|
||||
try {
|
||||
const t = token || getToken();
|
||||
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||
if (!payload.registerTime) return '';
|
||||
const date = new Date(payload.registerTime);
|
||||
return date.toISOString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
84
electron-vue-template/update-helper.bat
Normal file
@@ -0,0 +1,84 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set APP_ASAR=%~1
|
||||
set UPDATE_FILE=%~2
|
||||
set JAR_UPDATE=%~3
|
||||
set EXE_PATH=%~4
|
||||
set UPDATE_DIR=%~5
|
||||
|
||||
if not exist "%UPDATE_FILE%" if "%JAR_UPDATE%"=="" exit /b 1
|
||||
if not exist "%UPDATE_FILE%" if not exist "%JAR_UPDATE%" exit /b 1
|
||||
|
||||
REM Wait for application to close
|
||||
for /f "tokens=*" %%a in ("%EXE_PATH%") do set EXE_NAME=%%~nxa
|
||||
set COUNT=0
|
||||
:wait_loop
|
||||
tasklist /FI "IMAGENAME eq %EXE_NAME%" 2>nul | find /I "%EXE_NAME%" >nul
|
||||
if errorlevel 1 goto process_closed
|
||||
set /a COUNT+=1
|
||||
if %COUNT% GEQ 20 goto process_closed
|
||||
timeout /t 1 /nobreak >nul
|
||||
goto wait_loop
|
||||
:process_closed
|
||||
timeout /t 1 /nobreak >nul
|
||||
|
||||
REM Update ASAR
|
||||
if exist "%UPDATE_FILE%" (
|
||||
if exist "%APP_ASAR%.backup" del /f /q "%APP_ASAR%.backup" >nul 2>&1
|
||||
if exist "%APP_ASAR%" move /y "%APP_ASAR%" "%APP_ASAR%.backup" >nul 2>&1
|
||||
move /y "%UPDATE_FILE%" "%APP_ASAR%" >nul 2>&1
|
||||
if errorlevel 1 if exist "%APP_ASAR%.backup" move /y "%APP_ASAR%.backup" "%APP_ASAR%" >nul 2>&1
|
||||
if exist "%UPDATE_FILE%" del /f /q "%UPDATE_FILE%" >nul 2>&1
|
||||
)
|
||||
|
||||
REM Update JAR
|
||||
:update_jar
|
||||
if "%JAR_UPDATE%"=="" goto :start_app
|
||||
if not exist "%JAR_UPDATE%" goto :start_app
|
||||
timeout /t 3 /nobreak >nul
|
||||
for %%I in ("%APP_ASAR%") do set RESOURCES_DIR=%%~dpI
|
||||
for %%F in ("%JAR_UPDATE%") do set JAR_NAME=%%~nF
|
||||
echo %JAR_NAME% | findstr /B /C:"erp_client_sb-" >nul
|
||||
if errorlevel 1 (
|
||||
for /f "tokens=1-3 delims=/ " %%a in ("%date%") do set TODAY=%%a%%b%%c
|
||||
for /f "tokens=1-3 delims=:." %%a in ("%time%") do set NOW=%%a%%b%%c
|
||||
set JAR_NAME=erp_client_sb-2.4.7-!TODAY!!NOW!.jar
|
||||
)
|
||||
|
||||
REM Delete old JAR files
|
||||
for %%F in ("%RESOURCES_DIR%erp_client_sb-*.jar") do (
|
||||
set RETRY_COUNT=0
|
||||
:retry_delete
|
||||
del /f /q "%%F" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
set /a RETRY_COUNT+=1
|
||||
if !RETRY_COUNT! LEQ 5 (
|
||||
timeout /t 2 /nobreak >nul
|
||||
goto :retry_delete
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM Install new JAR file
|
||||
set NEW_JAR_PATH=%RESOURCES_DIR%%JAR_NAME%
|
||||
set INSTALL_RETRY=0
|
||||
:retry_install
|
||||
move /y "%JAR_UPDATE%" "%NEW_JAR_PATH%" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
set /a INSTALL_RETRY+=1
|
||||
if %INSTALL_RETRY% LEQ 5 (
|
||||
timeout /t 2 /nobreak >nul
|
||||
goto :retry_install
|
||||
)
|
||||
goto :start_app
|
||||
)
|
||||
if exist "%JAR_UPDATE%" del /f /q "%JAR_UPDATE%" >nul 2>&1
|
||||
|
||||
:start_app
|
||||
REM Clean up update directory
|
||||
if exist "%UPDATE_DIR%" (
|
||||
for %%F in ("%UPDATE_DIR%\*") do del /f /q "%%F" >nul 2>&1
|
||||
)
|
||||
start "" "%EXE_PATH%"
|
||||
exit /b 0
|
||||
@@ -1,14 +1,12 @@
|
||||
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, 'src', 'renderer', 'public'), // 使用renderer下的public目录
|
||||
publicDir: Path.join(__dirname, 'public'),
|
||||
server: {
|
||||
port: 8083,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>com.tashow.erp</groupId>
|
||||
<artifactId>erp_client_sb</artifactId>
|
||||
<version>2.4.7</version>
|
||||
<version>2.6.3</version>
|
||||
<name>erp_client_sb</name>
|
||||
<description>erp客户端</description>
|
||||
<properties>
|
||||
@@ -37,7 +37,6 @@
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>4.1.2</version>
|
||||
</dependency>
|
||||
<!-- 已移除 JavaFX/FxWeaver 相关依赖,保留为纯 Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>com.qiniu</groupId>
|
||||
<artifactId>qiniu-java-sdk</artifactId>
|
||||
@@ -55,9 +54,7 @@
|
||||
<artifactId>webmagic-extension</artifactId>
|
||||
<version>1.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFX 相关依赖已移除 -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@@ -67,7 +64,12 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
<version>5.8.36</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-poi</artifactId>
|
||||
<version>5.8.36</version>
|
||||
</dependency>
|
||||
<!-- SQLite数据库支持 -->
|
||||
@@ -95,12 +97,6 @@
|
||||
<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>
|
||||
@@ -120,6 +116,18 @@
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- OSHI for hardware information -->
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.4.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.36</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -167,6 +175,8 @@
|
||||
</exclude>
|
||||
</excludes>
|
||||
<mainClass>com.tashow.erp.ErpClientSbApplication</mainClass>
|
||||
<executable>false</executable>
|
||||
<layout>ZIP</layout>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
package com.tashow.erp;
|
||||
|
||||
import com.tashow.erp.utils.ErrorReporter;
|
||||
import com.tashow.erp.utils.ResourcePreloader;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -8,28 +7,22 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(
|
||||
exclude = {
|
||||
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration.class,
|
||||
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration.class
|
||||
}
|
||||
)
|
||||
public class ErpClientSbApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
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("Started Success");
|
||||
} catch (Exception e) {
|
||||
log.warn("未设置 ErrorReporter,继续启动: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
ResourcePreloader.init();
|
||||
ResourcePreloader.preloadErpDashboard();
|
||||
ResourcePreloader.executePreloading();
|
||||
} catch (Throwable t) {
|
||||
log.warn("资源预加载失败: {}", t.getMessage());
|
||||
}
|
||||
ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class);
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
|
||||
log.error("捕获到未处理异常: " + ex.getMessage(), ex);
|
||||
errorReporter.reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex);
|
||||
});
|
||||
ResourcePreloader.init();
|
||||
ResourcePreloader.preloadErpDashboard();
|
||||
ResourcePreloader.executePreloading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* 1688业务常量
|
||||
*/
|
||||
public class Alibaba1688Constants {
|
||||
public static final String APP_ID = "32517";
|
||||
public static final String INTERFACE_NAME = "imageOfferSearchService";
|
||||
public static final String APP_NAME = "ios";
|
||||
public static final String SEARCH_SCENE = "image";
|
||||
public static final String SEO_SCENE = "seoSearch";
|
||||
public static final int PAGE_SIZE = 40;
|
||||
public static final String JSV_VERSION = "2.6.1";
|
||||
public static final String API_VERSION = "2.0";
|
||||
public static final String DATA_TYPE = "json";
|
||||
public static final int TIMEOUT_MS = 10000;
|
||||
public static final String API_BASE = "https://h5api.m.1688.com/h5";
|
||||
public static final String API_METHOD = "mtop.relationrecommend.WirelessRecommend.recommend";
|
||||
|
||||
private Alibaba1688Constants() {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* 亚马逊业务常量
|
||||
*/
|
||||
public class AmazonConstants {
|
||||
public static final String REGION_JP = "JP";
|
||||
public static final String REGION_US = "US";
|
||||
public static final String DOMAIN_JP = "https://www.amazon.co.jp";
|
||||
public static final String DOMAIN_US = "https://www.amazon.com";
|
||||
public static final String URL_PRODUCT_PATH = "/dp/";
|
||||
public static final String SESSION_PREFIX = "SINGLE_";
|
||||
public static final String DATA_TYPE = "AMAZON";
|
||||
public static final int RETRY_TIMES = 3;
|
||||
public static final int SLEEP_TIME_BASE = 2000;
|
||||
public static final int SLEEP_TIME_RANDOM = 2000;
|
||||
public static final int TIMEOUT_MS = 20000;
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
public static final String HEADER_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8";
|
||||
public static final String HEADER_ACCEPT_ENCODING = "gzip, deflate, br";
|
||||
public static final String HEADER_ACCEPT_LANGUAGE_US = "zh-CN,zh;q=0.9,en;q=0.8";
|
||||
public static final String HEADER_ACCEPT_LANGUAGE_JP = "ja,en;q=0.9,zh-CN;q=0.8";
|
||||
public static final String HEADER_CACHE_CONTROL = "max-age=0";
|
||||
public static final String COOKIE_I18N_PREFS_US = "USD";
|
||||
public static final String COOKIE_I18N_PREFS_JP = "JPY";
|
||||
public static final String COOKIE_LC_US = "en_US";
|
||||
public static final String COOKIE_LC_JP = "zh_CN";
|
||||
public static final String COOKIE_SESSION_ID_US = "134-6097934-2082600";
|
||||
public static final String COOKIE_SESSION_ID_JP = "358-1261309-0483141";
|
||||
public static final String COOKIE_SESSION_ID_TIME = "2082787201l";
|
||||
public static final String COOKIE_UBID_US = "132-7547587-3056927";
|
||||
public static final String COOKIE_UBID_JP = "357-8224002-9668932";
|
||||
public static final String COOKIE_SKIN = "noskin";
|
||||
|
||||
private AmazonConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* 斑马业务常量
|
||||
*/
|
||||
public class BanmaConstants {
|
||||
public static final String API_BASE = "https://banma365.cn";
|
||||
public static final String API_ORDER_LIST = API_BASE + "/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
|
||||
public static final String API_ORDER_LIST_WITH_TIME = API_BASE + "/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
|
||||
public static final String API_TRACKING = API_BASE + "/zebraExpressHub/web/tracking/getByExpressNumber/%s";
|
||||
public static final String API_SHOP_LIST = API_BASE + "/api/shop/list?_t=%d";
|
||||
public static final int CONNECT_TIMEOUT_SECONDS = 5;
|
||||
public static final int READ_TIMEOUT_SECONDS = 10;
|
||||
public static final String DATA_TYPE = "BANMA";
|
||||
public static final String DATA_TYPE_CACHE = "BANMA_CACHE";
|
||||
public static final String TRACKING_PREFIX_ORDER = "ORDER_";
|
||||
public static final String TRACKING_PREFIX_PRODUCT = "PRODUCT_";
|
||||
public static final String TRACKING_PREFIX_UNKNOWN = "UNKNOWN_";
|
||||
public static final String SESSION_PREFIX = "SESSION_";
|
||||
public static final int CACHE_HOURS = 1;
|
||||
|
||||
private BanmaConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* 缓存相关常量
|
||||
*/
|
||||
public class CacheConstants {
|
||||
public static final int DATA_RETENTION_HOURS = 1;
|
||||
public static final int TRADEMARK_CACHE_DAYS = 1;
|
||||
public static final int SESSION_LIMIT = 1;
|
||||
public static final int RAKUTEN_CACHE_HOURS = 1;
|
||||
|
||||
private CacheConstants() {}
|
||||
}
|
||||
@@ -1,169 +1,16 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 通用常量信息
|
||||
*
|
||||
* @author ruoyi
|
||||
* 通用常量(保留兼容性)
|
||||
* 新代码请使用具体业务常量类:AmazonConstants、RakutenConstants、HttpConstants等
|
||||
*/
|
||||
public class Constants
|
||||
{
|
||||
/**
|
||||
* UTF-8 字符集
|
||||
*/
|
||||
public static final String UTF8 = "UTF-8";
|
||||
@Deprecated
|
||||
public class Constants {
|
||||
public static final String HTTP = HttpConstants.HTTP;
|
||||
public static final String HTTPS = HttpConstants.HTTPS;
|
||||
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
|
||||
|
||||
/**
|
||||
* 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" };
|
||||
private Constants() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* 方舟精选业务常量
|
||||
*/
|
||||
public class FangzhouConstants {
|
||||
public static final String API_URL = "https://api.fangzhoujingxuan.com/Task";
|
||||
public static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
|
||||
public static final int TOKEN_EXPIRED_CODE = -1006;
|
||||
public static final String WEBSITE_CODE = "1";
|
||||
public static final int SUCCESS_CODE = 1;
|
||||
|
||||
private FangzhouConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* HTTP协议常量
|
||||
*/
|
||||
public class HttpConstants {
|
||||
public static final String HTTP = "http://";
|
||||
public static final String HTTPS = "https://";
|
||||
public static final String CHARSET_UTF8 = "UTF-8";
|
||||
public static final String CHARSET_GBK = "GBK";
|
||||
|
||||
private HttpConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.tashow.erp.common;
|
||||
|
||||
/**
|
||||
* 乐天业务常量
|
||||
*/
|
||||
public class RakutenConstants {
|
||||
public static final String DATA_TYPE = "RAKUTEN";
|
||||
public static final String DOMAIN = "https://item.rakuten.co.jp";
|
||||
public static final int RETRY_TIMES = 3;
|
||||
public static final int SLEEP_TIME_BASE = 2000;
|
||||
public static final int SLEEP_TIME_RANDOM = 2000;
|
||||
public static final int TIMEOUT_MS = 20000;
|
||||
public static final String DATA_TYPE_CACHE = "RAKUTEN_CACHE";
|
||||
|
||||
private RakutenConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.tashow.erp.config;
|
||||
import com.tashow.erp.utils.SeleniumUtil;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
/**
|
||||
* ChromeDriver 配置类
|
||||
* 已禁用预加载,由 TrademarkCheckUtil 按需创建
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@Order(100)
|
||||
public class ChromeDriverPreloader implements ApplicationRunner {
|
||||
|
||||
private ChromeDriver globalDriver;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 不再预加载,节省资源
|
||||
log.info("ChromeDriver 配置已加载(按需启动)");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChromeDriver chromeDriver() {
|
||||
// 为兼容性保留 Bean,但不自动创建
|
||||
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(true);
|
||||
return globalDriver;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (globalDriver != null) {
|
||||
try {
|
||||
globalDriver.quit();
|
||||
} catch (Exception e) {
|
||||
log.error("关闭ChromeDriver失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ public class ErrorReportAspect {
|
||||
// 检查返回值是否表示错误
|
||||
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() : "未知错误";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
@@ -1,13 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||