Compare commits

...

54 Commits

Author SHA1 Message Date
358203b11d feat(splash): 添加全局开屏图片功能并优化客户端启动流程
- 新增全局开屏图片上传、获取、删除接口
- 实现客户端全局开屏图片优先加载机制
- 集成七牛云存储配置从 pxdj 切换到 bydj
- 优化 splash 窗口显示逻辑确保至少显示 2 秒
- 添加全局开屏图片管理界面组件
- 更新应用配置和构建设置
- 修复多处图片缓存和加载问题
- 调整服务端 API 地址配置
- 修改微信客服联系方式
2026-01-19 17:33:43 +08:00
02858146b3 style(components): format CSS styles in Vue components
- Remove extra spaces in CSS property declarations
- Consolidate multi-line CSS rules into single lines
- Maintain consistent formatting across component styles
- Improve readability by removing unnecessary line breaks
- Ensure uniform styling structure in scoped CSS blocks
2025-11-28 17:14:00 +08:00
bff057c99b feat(auth):从JWT令牌中提取注册时间
- 在token工具函数中新增getRegisterTimeFromToken方法
- 修改客户端账户注册逻辑,将创建时间写入JWT令牌- 更新前后端代码以正确传递和解析registerTime字段
- 调整API调用逻辑,优先从令牌中获取注册时间
- 清理部分冗余代码和注释
2025-11-18 09:45:22 +08:00
d29d4d69da feat(erp):优化品牌商标缓存与方舟API调用逻辑
- 品牌商标缓存服务增加一天内去重保存逻辑- 方舟API调用支持TOKEN失效自动重新注册
- 增加证书验证失败重试机制
- 修复代理池API签名密钥
-优化商标检查面板完成状态计算逻辑- 更新应用配置注释格式
2025-11-17 14:34:08 +08:00
937a84bb81 fix(settings): 更新品牌 Banner 尺寸提示信息
- 将品牌 Banner 最佳显示尺寸从 1200*736 更新为 20*64
- 调整图片预览区域以适配新尺寸比例

---

test(api): 修改商标查询测试用例参数

- 更改测试品牌名称从 SummitFlare 为 MADCKDEDRT- 简化测试输出内容,仅显示注册状态结果
2025-11-14 17:54:04 +08:00
f9d1848280 feat(amazon):优化商标筛查重试机制和进度显示- 添加防抖控制,避免频繁点击重试按钮
- 优化重试逻辑,增加时间间隔限制和状态检查
- 移除表格上方冗余的进度条显示
- 更新取消状态下的提示文案和操作引导- 修复品牌统计数据显示逻辑,确保准确性- 调整用户界面元素间距和样式细节
- 完善后端接口调用,支持信号中断和错误处理
-优化SSE连接管理,防止连接泄漏
- 改进任务取消机制,提升用户体验
- 更新用户信息展示,增加注册时间显示
2025-11-14 17:22:25 +08:00
dd23d9fe90 feat(amazon):优化商标筛查面板状态指示器与进度逻辑
- 将 SVG 图标替换为 PNG 图片以提升渲染性能
- 调整状态图标动画效果及连接线样式- 修改任务进度初始值以即时反映进行中状态
- 动态计算配置步骤总数并更新显示逻辑- 移除开发模式下的调试快捷键与相关日志- 微调 CSS 样式以改善界面布局与视觉效果
2025-11-13 15:33:02 +08:00
007799fb2a feat(trademark):优化商标查询功能和Excel解析逻辑
- 重构品牌商标缓存服务,移除冗余的日志记录和存在检查- 简化Excel解析工具类,提取公共方法并优化列索引查找逻辑
- 增强Electron客户端开发模式下的后端启动控制能力
- 改进商标筛查面板的用户体验和数据处理流程-优化商标查询工具类,提高查询准确性和稳定性
- 调整商标控制器接口参数校验逻辑和资源清理机制
- 更新USPTO API测试用例以支持Spring容器环境运行
2025-11-13 14:20:12 +08:00
cfb9096788 feat(amazon): 更新商标筛查界面图标与状态展示- 将商标筛查状态图标从图片替换为 SVG 图标
- 添加了进行中、取消、完成/失败状态的 SVG 图标
-优化任务进度指示器,使用 SVG 并支持旋转动画
- 禁用跟卖许可筛查功能并更新提示文案
- 调整标签页样式和间距,适配不同屏幕尺寸
-修复状态横幅图标显示问题,并统一图标尺寸
- 更新全局样式以提升视觉一致性和用户体验
2025-11-12 15:55:06 +08:00
cce281497b feat(client): 添加品牌logo功能支持
- 在客户端账户实体中新增brandLogo字段用于存储品牌logo URL
- 实现品牌logo的上传、获取和删除接口
- 在Vue前端中集成品牌logo的展示和管理功能- 添加品牌logo的缓存机制提升访问性能
- 在设置对话框中增加品牌logo配置界面
- 实现品牌logo的预览、上传和删除操作
- 添加VIP权限控制确保只有VIP用户可使用该功能
- 增加品牌logo变更事件监听以实时更新界面显示- 更新数据库映射文件以支持brand_logo字段的读写- 在登录成功后异步加载品牌logo配置信息- 调整UI布局以适配品牌logo展示区域- 添加品牌logo相关的样式定义和响应式处理
- 实现品牌logo上传的文件类型和大小校验- 增加品牌logo删除确认提示增强用户体验
- 在App.vue中添加品牌logo的全局状态管理和展示逻辑- 优化品牌logo加载失败时的容错处理
- 完善品牌logo功能的相关错误处理和日志记录
2025-11-10 15:18:38 +08:00
92ab782943 refactor(utils):优化商标查询重试机制- 将初始化失败重试次数从3次增加到5次
- 改进批量查询逻辑,支持单个品牌独立重试- 添加针对HTTP 403、网络错误等异常的代理切换机制-优化查询脚本构造方式,提高执行稳定性
- 增强错误处理和日志输出信息
- 移除控制器中冗余的注释描述
2025-11-10 11:21:04 +08:00
c2e1617a99 feat(client): 实现自定义开屏图片功能
- 在 ClientAccount 实体中新增 splashImage 字段用于存储开屏图片URL
- 在 ClientAccountController 中添加上传、获取和删除开屏图片的接口
- 集成七牛云存储实现图片上传功能,支持图片格式和大小校验
- 使用 Redis 缓存开屏图片URL,提升访问性能
- 在客户端登录成功后异步加载并保存开屏图片配置
- 新增 splashApi 模块封装开屏图片相关HTTP请求- 在主进程中实现开屏图片配置的持久化存储和读取
- 在设置页面中增加开屏图片管理界面,支持上传、预览和删除操作
- 修改 splash.html 支持动态加载自定义开屏图片
- 调整 CSP 策略允许加载本地和HTTPS图片资源
2025-11-08 10:23:45 +08:00
7c7009ffed feat(electron):优化商标筛查面板与资源加载逻辑
- 将多个 v-if 条件渲染改为 v-show,提升组件切换性能
- 优化商标任务完成状态判断逻辑,确保准确显示采集完成图标- 调整任务统计数据显示条件,支持零数据展示- 更新 API 配置地址,切换至本地开发环境地址
- 降低 Spring Boot 线程池与数据库连接池配置,适应小规模并发- 禁用 devtools 热部署与 Swagger 接口文档,优化生产环境性能
- 配置 RestTemplate 使用 HttpClient 连接池,增强 HTTP 请求稳定性
- 改进静态资源拷贝脚本,确保 icon 与 image 文件夹正确复制
- 更新 electron-builder 配置,优化资源打包路径与应用图标
- 修改 HTTP 路由规则,明确区分客户端与管理端接口路径- 注册文件协议拦截器,解决生产环境下 icon/image 资源加载问题
- 调整商标 API 接口路径,指向 erp_client_sb服务
-重构 MarkController 控制器,专注 Token 管理功能
- 优化线程池参数,适配低并发业务场景- 强化商标筛查流程控制,完善任务取消与异常处理机制
- 新增方舟精选任务管理接口,实现 Excel 下载与数据解析功能
2025-11-07 11:30:24 +08:00
2f00fde3be feat(electron):优化商标筛查面板与资源加载逻辑
- 将多个 v-if 条件渲染改为 v-show,提升组件切换性能
- 优化商标任务完成状态判断逻辑,确保准确显示采集完成图标- 调整任务统计数据显示条件,支持零数据展示- 更新 API 配置地址,切换至本地开发环境地址
- 降低 Spring Boot 线程池与数据库连接池配置,适应小规模并发- 禁用 devtools 热部署与 Swagger 接口文档,优化生产环境性能
- 配置 RestTemplate 使用 HttpClient 连接池,增强 HTTP 请求稳定性
- 改进静态资源拷贝脚本,确保 icon 与 image 文件夹正确复制
- 更新 electron-builder 配置,优化资源打包路径与应用图标
- 修改 HTTP 路由规则,明确区分客户端与管理端接口路径- 注册文件协议拦截器,解决生产环境下 icon/image 资源加载问题
- 调整商标 API 接口路径,指向 erp_client_sb服务
-重构 MarkController 控制器,专注 Token 管理功能
- 优化线程池参数,适配低并发业务场景- 强化商标筛查流程控制,完善任务取消与异常处理机制
- 新增方舟精选任务管理接口,实现 Excel 下载与数据解析功能
2025-11-06 14:39:58 +08:00
cfb70d5830 feat(amazon): 实现商标筛查功能并优化用户体验
- 添加商标筛查面板和相关API接口- 实现Excel文件解析和数据过滤功能
- 添加文件上传进度跟踪和错误处理-优化空状态显示和操作引导- 实现tab状态持久化存储
- 添加订阅会员弹窗和付费入口
-优化文件选择和删除功能
- 改进UI样式和响应式布局
2025-11-06 11:07:05 +08:00
4e2ce48934 feat(trademark): 支持商标筛查任务取消状态及优化错误处理- 新增商标筛查取消状态的UI展示和处理逻辑
-优化错误提示信息,区分网络错误、超时和风控场景
- 改进任务进度计算逻辑,支持更准确的完成状态判断
- 调整品牌提取逻辑,从Excel中直接读取品牌列数据
- 增强后端403错误检测和代理自动切换机制
- 更新前端组件样式和交互逻辑以匹配新状态
-修复部分条件判断逻辑以提升稳定性- 调整文件上传大小限制至50MB以支持更大文件- 优化Excel解析工具类,支持自动识别表头行位置
2025-11-05 10:16:14 +08:00
a62d7b6147 feat(trademark): 实现商标筛查功能并优化相关配置
- 新增商标筛查进度展示界面与交互逻辑
- 实现产品、品牌及平台跟卖许可的分项任务进度追踪
- 添加商标数据导出与任务重试、取消功能
- 调整Redis连接池配置以提升并发性能
- 禁用ChromeDriver预加载,改为按需启动以节省资源- 支持品牌商标远程筛查接口调用与结果解析
- 增加Hutool工具库依赖用于简化IO与Excel处理- 更新USPTO商标查询脚本实现自动化检测
- 修改Ruoyi后台Redis依赖版本并添加集群心跳配置- 切换本地开发环境API地址指向内网测试服务器
2025-11-04 15:39:15 +08:00
c9874f1786 feat(amazon):重构亚马逊仪表板组件并新增商标筛查功能
- 将原有复杂逻辑拆分为独立组件:AsinQueryPanel、GenmaiSpiritPanel、TrademarkCheckPanel
- 新增商标批量筛查功能,支持商标状态、类别、权利人等信息查询
- 优化UI布局,改进标签页样式和响应式设计
- 重构数据处理逻辑,使用计算属性优化性能- 完善分页功能,支持不同tab的数据展示
- 移除冗余代码,提高组件可维护性
- 添加跟卖精灵功能说明和注意事项展示-优化空状态和加载状态的用户体验
2025-10-31 11:30:19 +08:00
87a4a2fed0 feat(system): 新增方舟账号管理与任务接口
- 新增 MarkController 控制器,提供方舟任务列表获取和新建任务接口
- 实现 IMarkService 接口及 MarkServiceImpl 实现类,支持注册、登录和MD5加密功能
- 在 CacheConstants 中添加方舟账号 Redis key 常量
- 集成 RestTemplate用于调用方舟API,支持自动注册与登录机制- 添加文件上传支持,用于新建任务时提交文件
- 实现 token 失效自动重新登录逻辑,确保接口调用稳定性
2025-10-30 13:55:07 +08:00
d0a930d4f2 feat(selenium):重构ChromeDriver预加载与防检测配置
- 移除旧的WebDriverManager配置逻辑
- 新增SeleniumStealthUtil工具类,集成防检测脚本
- 实现全局单例ChromeDriver Bean管理- 添加驱动生命周期自动清理机制
-优化驱动创建参数,增强浏览器伪装能力
- 移除无用的线程池销毁方法
- 调整配置类注解与加载顺序
2025-10-29 16:36:34 +08:00
6443cdc8d0 feat(electron):优化应用启动和健康检查逻辑
- 修改 Spring Boot 配置启用懒加载初始化
- 优化主进程窗口打开逻辑,增加销毁状态检查
- 简化数据迁移函数中的条件判断
- 添加 JVM 参数 UseSerialGC优化内存使用- 移除 Spring 进程的标准输出和错误流监听- 改进健康检查机制,使用版本接口确认服务就绪
- 调整启动超时时间并优化重试间隔
- 延迟更新检查时机以提升启动速度
2025-10-28 11:05:53 +08:00
1aceceb38f feat(electron):优化跟卖精灵启动流程和UI细节
- 移除启动时的固定延时,改为即时反馈启动状态- 更新跟卖精灵描述文案,移除初始化时间说明
- 统一spinner样式类名,优化加载动画显示逻辑
- 调整菜单激活背景色,增强视觉层次感- 引入平台图标图片替代文字标识- 修改VIP状态栏背景及文字颜色,提升可读性
- 配置ChromeDriver国内镜像源并实现后台预加载
- 添加WebDriverManager依赖以自动管理浏览器驱动
-优化electron-builder资源打包配置
2025-10-28 09:39:59 +08:00
84087ddf80 feat(client): 实现跟卖精灵异步启动和Chrome驱动预加载- 在GenmaiServiceImpl中添加@Async注解实现异步启动跟卖精灵- 增加ChromeDriverPreloader组件预加载Chrome驱动- 添加AsyncConfig配置类启用异步支持
- 优化跟卖精灵启动提示信息和加载状态显示
- 移除Java代码中关于刷新令牌的相关逻辑和依赖- 更新版本号从2.5.5到2.5.6
2025-10-27 16:49:37 +08:00
7e065c1a0b feat(erp): 实现按用户地区隔离的最新产品查询逻辑
- 修改AmazonController以支持按用户和区域筛选最新会话数据
- 更新AmazonProductRepository中的findLatestProducts方法,增加region参数实现数据隔离
-优化AmazonScrapingServiceImpl的数据处理流程,增强缓存清理机制
- 调整GenmaiServiceImpl的token验证逻辑并改进Chrome启动配置- 升级系统版本至2.5.5并完善相关依赖管理
- 改进前端设置对话框中关于缓存清理描述的信息准确性
-重构SystemController接口,移除不必要的用户名参数传递- 强化GenmaiAccountController和服务层的安全校验逻辑
2025-10-27 13:34:25 +08:00
0be60bc103 feat(genmai): 集成跟卖精灵账号管理系统跟
- 新增卖精灵账号管理功能,支持多账号切换
- 实现账号增删改查接口与前端交互逻辑
-优化打开跟卖精灵流程,增加账号选择界面
- 添加账号权限限制与订阅升级提醒
- 完善后端账号实体类及数据库映射
- 更新系统控制器以支持指定账号启动功能- 调整HTTP请求路径适配新工具模块路由- 升级客户端版本至2.5.3并优化代码结构
2025-10-27 09:13:00 +08:00
35c9fc205a featlectron): 实(e现自定义窗口控制和无边框窗口
- 添加窗口最小化、最大化、关闭和状态检测 API- 实现无边框窗口模式并添加窗口控制按钮
- 更新导航栏 UI,添加登录按钮和窗口控制区域
- 调整 Amazon、Rakuten 和 Zebra 仪表板样式
- 移除面包屑导航并调整布局结构
- 更新用户头像样式和 VIP 状态卡片背景渐变
- 添加窗口拖拽区域和用户选择禁用样式
2025-10-24 15:06:47 +08:00
3a76aaa3c0 feat(client): 实现用户数据隔离与设备绑定优化- 添加用户会话ID构建逻辑,确保数据按用户隔离- 优化设备绑定流程,支持设备状态更新和绑定时间同步- 实现用户缓存清理功能,仅清除当前用户的数据- 增强客户端账号删除逻辑,级联删除相关数据
- 调整设备在线查询逻辑,确保只返回活跃绑定的设备
- 优化试用期逻辑,精确计算过期时间和类型- 添加账号管理弹窗和相关状态注入
-修复跟卖精灵按钮加载状态显示问题
- 增强文件上传区域UI,显示选中文件名
- 调整分页组件样式,优化界面展示效果- 优化反馈日志存储路径逻辑,默认使用用户目录
- 移除冗余代码和无用导入,提升代码整洁度
2025-10-24 13:43:46 +08:00
e2a438c84e del 2025-10-22 14:19:10 +08:00
5468dc53fc feat(device): 更新设备管理功能并优化错误处理- 修改设备更新接口路径从 /updateExpire 到 /update- 添加设备注册时获取计算机名称功能
- 优化设备配额检查逻辑,增加账号存在性验证- 更新前端设备列表刷新逻辑,使用保存的用户名参数
- 修改账号编辑表单,禁用已存在账号的用户名和账号名编辑
-优化跟卖精灵打开功能的错误提示和异常处理- 添加页面刷新 IPC通信功能
- 限制用户名输入只能包含字母、数字和下划线
- 移除冗余的本地 IP 获取函数- 升级 erp_client_sb 模块版本至 2.4.9
2025-10-22 14:18:28 +08:00
17b6a7b9f9 feat(device): 实现设备与账号绑定管理机制
- 引入 ClientAccountDevice 表管理设备与账号绑定关系
- 重构设备注册逻辑,支持多账号绑定同一设备
- 新增设备配额检查,基于账号维度限制设备数量
-优化设备移除逻辑,仅解除绑定而非物理删除- 改进设备列表查询,通过账号ID关联获取设备信息
- 更新心跳任务,支持向设备绑定的所有账号发送心跳
- 调整设备API参数,增加username字段用于权限校验
-修复HTTP请求编码问题,统一使用UTF-8字符集
- 增强错误处理,携带错误码信息便于前端识别
- 移除设备表中的username字段,解耦设备与用户名关联
2025-10-22 09:51:55 +08:00
901d67d2dc feat(subscription): 添加订阅功能并优化过期处理逻辑
- 扩展 trialExpiredType 类型,新增 'subscribe' 状态以支持主动订阅场景
- 新增 openSubscriptionDialog 方法,用于处理 VIP 状态点击事件
- 优化 VIP 状态卡片 UI,添加悬停与点击效果,提升交互体验
- 调整过期状态样式,保持水平布局并移除冗余按钮样式
- 在 Rakuten 组件中引入请求中断机制,提升任务控制灵活性- 更新 TrialExpiredDialog 组件,支持订阅类型提示与微信复制反馈- 修复部分 API 调用未传递 signal 参数的问题,增强请求管理能力
- 切换 Ruoyi 服务地址至生产环境配置,确保接口通信正常
- 移除部分无用代码与样式,精简组件结构
2025-10-21 11:48:32 +08:00
1be22664c4 feat(subscription): 添加订阅功能并优化过期处理逻辑
- 扩展 trialExpiredType 类型,新增 'subscribe' 状态以支持主动订阅场景
- 新增 openSubscriptionDialog 方法,用于处理 VIP 状态点击事件
- 优化 VIP 状态卡片 UI,添加悬停与点击效果,提升交互体验
- 调整过期状态样式,保持水平布局并移除冗余按钮样式
- 在 Rakuten 组件中引入请求中断机制,提升任务控制灵活性- 更新 TrialExpiredDialog 组件,支持订阅类型提示与微信复制反馈- 修复部分 API 调用未传递 signal 参数的问题,增强请求管理能力
- 切换 Ruoyi 服务地址至生产环境配置,确保接口通信正常
- 移除部分无用代码与样式,精简组件结构
2025-10-21 11:33:41 +08:00
281ae6a846 refactor(api):重构API服务接口与实现
- 移除多余的接口定义文件,简化依赖关系- 更新控制器和服务实现类的注入方式-优化请求参数处理逻辑
- 统一响应数据结构格式- 调整方法签名以提高一致性
- 删除冗余注释和无用代码- 修改系统API调用路径引用位置
- 简化认证服务实现并移除不必要的抽象层
- 优化Excel文件解析相关功能
- 清理无用的工具类和配置项
- 调整错误上报机制的依赖注入方式
- 更新跟卖精灵服务的实现细节- 优化HTTP请求工具函数结构
- 移除废弃的缓存管理服务接口定义
- 调整设备配额检查逻辑复用性
- 优化订单服务的数据返回格式
- 更新产品服务中的数据处理方式
- 重构客户端账户控制器中的设备限制检查逻辑
2025-10-21 10:15:33 +08:00
17f03c3ade refactor(auth):重构认证服务并移除冗余代码
- 移除了 AuthServiceImpl 中的登录、注册、token 验证等方法,仅保留错误上报和客户端信息功能
- 删除了设备注册和离线通知相关逻辑
- 移除了 IAuthService 接口中的登录、注册、验证 token 等方法定义
- 清理了 AccountManager.vue 中的无关注释文字-优化了阿里巴巴1688 服务中的图片上传处理逻辑- 移除了 AmazonScrapingServiceImpl 中未使用的日志导入和空行
- 统一了 Vue 组件中的同步导入方式,替换异步组件定义
- 更新了应用配置文件中的服务器地址和懒加载设置
- 新增缓存管理服务用于统一清理各类缓存数据
- 优化了设备 IP 地址获取逻辑并在注册时传递给后端- 调整了构建配置以减小安装包体积并支持多语言
- 修改了主进程窗口加载逻辑以适配开发与生产环境- 添加了全局样式限制图片预览器尺寸
- 移除了设备 ID 测试类和部分无用的正则表达式导入
2025-10-20 18:01:40 +08:00
0c85aa5677 refactor(client):优化设备管理与登录逻辑
- 移除冗余的日志记录器声明
- 简化设备心跳接口,合并注册与更新逻辑
- 调整设备数量限制检查逻辑,提高代码可读性
- 修改默认设备数量限制从3台调整为1台- 更新客户端登出提示文案- 固定启动窗口尺寸并移除延迟启动逻辑
- 调整设备移除时的消息提示内容
2025-10-17 16:14:43 +08:00
d9f91b77e3 feat(electron): 实现系统托盘和关闭行为配置功能
- 添加系统托盘创建和销毁逻辑- 实现窗口关闭行为配置(退出/最小化/托盘)
- 添加配置文件读写功能
- 实现下载取消和清理功能
- 添加待更新文件检查机制
- 优化文件下载进度和错误处理
- 添加自动更新配置选项- 实现平滑滚动动画效果
- 添加试用期过期类型检查
-优化VIP状态刷新逻辑
2025-10-17 14:18:01 +08:00
07e34c35c8 feat(electron): 实现系统托盘和关闭行为配置功能
- 添加系统托盘创建和销毁逻辑- 实现窗口关闭行为配置(退出/最小化/托盘)
- 添加配置文件读写功能
- 实现下载取消和清理功能
- 添加待更新文件检查机制
- 优化文件下载进度和错误处理
- 添加自动更新配置选项- 实现平滑滚动动画效果
- 添加试用期过期类型检查
-优化VIP状态刷新逻辑
2025-10-17 14:17:47 +08:00
6e1b4d00de feat(client): 实现账号设备试用期管理功能
- 新增设备试用期过期时间字段及管理接口
- 实现试用期状态检查与过期提醒逻辑
- 支持账号类型区分试用与付费用户
- 添加设备注册时自动设置3天试用期- 实现VIP状态刷新与过期类型判断
-优化账号列表查询支持按客户端用户名过滤
- 更新客户端设备管理支持试用期控制- 完善登录流程支持试用期状态提示
-修复设备离线通知缺少用户名参数问题
- 调整账号默认设置清除逻辑关联客户端用户名
2025-10-17 14:17:02 +08:00
132299c4b7 feat(electron-vue-template):重构认证与设备管理模块
- 统一token存取逻辑,封装getToken/setToken/removeToken方法
-优化设备ID获取逻辑,调整API路径
- 完善设备管理接口类型定义,增强类型安全
- 调整SSE连接逻辑,使用统一配置管理- 重构HTTP客户端,集中管理后端服务配置
- 更新认证相关API接口,完善请求/响应类型
- 优化设备列表展示逻辑,移除冗余字段
- 调整图片代理路径,统一API前缀
- 完善用户反馈列表展示功能,增强交互体验
- 移除冗余的错误处理逻辑,简化代码结构
2025-10-16 10:37:00 +08:00
6f04658265 fix(client): 设备移除逻辑与认证流程优化
- 修改设备移除时的本地清理方法,统一调用 clearLocalAuth
- 优化设备数量限制校验逻辑,避免重复计算当前设备- 移除冗余的设备状态检查,简化设备移除流程- 调整 Redis 连接超时与等待时间,提升连接稳定性- 增强 MySQL 数据库连接配置,添加自动重连机制
-优化 Druid 连接池参数,提高数据库连接性能
- 简化客户端认证与数据上报逻辑,提升处理效率
- 移除过期设备状态更新逻辑,减少不必要的数据库操作- 调整慢 SQL 记录阈值,便于及时发现性能问题-优化版本分布与数据类型统计查询逻辑,提高响应速度
2025-10-15 18:32:48 +08:00
f614860eee feat(client): 实现稳定的设备ID生成与认证优化
- 重构设备ID生成逻辑,采用多重降级策略确保唯一性与稳定性- 移除客户端SQLite缓存依赖,改用localStorage存储token与设备ID
- 优化认证流程,简化token管理与会话恢复逻辑- 增加设备数量限制检查,防止超出配额
- 更新SSE连接逻辑,适配新的认证机制
- 调整Redis连接池配置,提升并发性能与稳定性
- 移除冗余的缓存接口与本地退出逻辑
- 修复设备移除时的状态处理问题,避免重复调用offline接口
- 引入OSHI库用于硬件信息采集(备用方案)- 更新开发环境API地址配置
2025-10-13 16:12:51 +08:00
4c2546733e 1 2025-10-10 15:37:57 +08:00
a0d94b2c70 feat(update): 添加版本更新提示红点功能
- 新增小红点标识新版本可用状态
- 实现点击版本号直接显示更新对话框- 优化更新逻辑,区分首次发现、跳过版本和稍后提醒状态- 添加脉冲动画效果增强视觉提示- 完善本地存储判断避免重复提示
2025-10-10 14:00:31 +08:00
d77ebab153 1 2025-10-10 13:39:16 +08:00
a28d846638 1 2025-10-10 11:28:04 +08:00
1c4dd2dd6d 1 2025-10-10 10:07:57 +08:00
6f22c9bffd 1 2025-10-10 10:06:56 +08:00
4fbe51d625 1 2025-10-09 11:18:26 +08:00
db67a99288 1 2025-10-09 10:02:37 +08:00
4065da3766 1 2025-10-09 09:27:21 +08:00
61235c5610 1 2025-09-30 17:19:34 +08:00
8d16d0b286 1 2025-09-30 17:18:23 +08:00
52ce0e1969 1 2025-09-30 17:16:11 +08:00
e650a7c7f3 1 2025-09-30 11:07:47 +08:00
273 changed files with 20659 additions and 32001 deletions

View File

@@ -1,48 +0,0 @@
---
description:
globs:
alwaysApply: false
---
---
description:
globs:
alwaysApply: false
---
# Your rule content
#角色
你是一名精通开发的高级工程师拥有10年以上的应用开发经验熟悉*等开发工具和技术栈。
你的任务是帮助用户设计和开发易用且易于推护的 *** 应用。始终遵循最佳实践,并坚持干净代码和健壮架构的原则。
#目标
你的目标是以用户容易理解的方式帮助他们完成“应用的设计和开发工作,确保应用功能完善、性能优异、用户体验良好。
#要求
在理解用户需求、设计UI、编写代码、解决问题和项目选代优化时你应该始终遵循以下原则:
##需求理解
-充分理解用户需求,站在用户角度思考,分析需求是否存在缺漏,并与用户讨论完善需求;
-选择最简单的解决方案来满足用户需求,避免过度设计。
##UI和样式设计
-使用现代UI框架进行样式设计(例如***这里可以根据不同开发项目仔纽展开比如使用哪些视觉规范或者UI框架没有的话也可以不用过多展开);
-在不同平台上实现一致的设计和响应式模式
##代码编写
技术选型:根据项目需求选择合适的技术栈(例如***,这里需要仔细展开,比如介招某个技术栈用在什么地方,以及要遵循什么最佳实践)
代码结构:强调代码的清晰性、模块化、可维护性,遵循最佳实践(如DRY原则、最小权限原则、响应式设计等)
-代码安全性:在编写代码时,始终考虑安全性,避免引入漏洞,确保用户输入的安全处理
-性能优化:优化代码的性能,减少资源占用,提升加载速度,确保项目的高效运行
-测试与文档:编写单元测试,确保代码的健壮性,并提供清晰的中文注释和文档。方便后续阅读和维护
##问题解决
-全面阅读相关代码,理解***应用的工作原理
-根据用户的反馈分析问题的原因,提出解决问题的思路
-确保每次代码变更不会破坏现有功能,且尽可能保持最小的改动
##迭代优化
与用户保持密切沟通,根据反读调整功能和设计,确保应用符合用户需求
在不确定需求时,主动询问用户以澄清需求或技术细节
##方法论
-系统2思维:以分析严谨的方式解决问题。将需求分解为更小、可管理的部分,并在实施前仔细考虑每一步
思维树:评估多种可能的解决方案及其后果。使用结构化的方法探索不同的路径。并选择最优的解决方案
-选代改进:在最终确定代码之前,考虑改进、边缘情况和优化。通过潜在增强的迭代,确保最终解决方案是健壮的

View File

@@ -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中的规范代码简洁度和性能优先。
💡 **操作提示**:在每次修改代码前,先向我说明修改的思路和方案,我确认同意后再进行代码更改。
---

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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>

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="332JslhtSnNRRZRMrLiHaPZ3q2S" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Maven.erp_client_sb [clean].executor&quot;: &quot;Run&quot;,
&quot;Maven.erp_client_sb [install].executor&quot;: &quot;Run&quot;,
&quot;Maven.erp_client_sb [verify].executor&quot;: &quot;Run&quot;,
&quot;Maven.ruoyi [clean].executor&quot;: &quot;Run&quot;,
&quot;Maven.ruoyi [install].executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RequestMappingsPanelOrder0&quot;: &quot;0&quot;,
&quot;RequestMappingsPanelOrder1&quot;: &quot;1&quot;,
&quot;RequestMappingsPanelWidth0&quot;: &quot;75&quot;,
&quot;RequestMappingsPanelWidth1&quot;: &quot;75&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;Spring Boot.ErpClientSbApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.RuoYiApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.未命名.executor&quot;: &quot;Debug&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/electron-vue-template/public&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.build.executor&quot;: &quot;Run&quot;,
&quot;npm.build:win.executor&quot;: &quot;Run&quot;,
&quot;npm.run.executor&quot;: &quot;Run&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.intellij.platform.ide.impl.presentationAssistant.PresentationAssistantConfigurable&quot;,
&quot;ts.external.directory.path&quot;: &quot;C:\\Users\\ZiJIe\\Desktop\\wox\\RuoYi-Vue\\electron-vue-template\\node_modules\\typescript\\lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</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
View File

@@ -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

View File

@@ -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.

View File

@@ -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);
```

View File

@@ -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"]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -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>

View 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;

View File

@@ -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()))
);

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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: () => {

View 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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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');
},
}
};

View File

@@ -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 })
}
}

View File

@@ -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)
}
}

View 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)
}
}

View 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`)
}
}

View File

@@ -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;
}
};

View 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 })
}
}

View File

@@ -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')
}
}

View File

@@ -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);
},
};

View 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')
}
}

View 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');
}
};

View 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}`)
}
}

View File

@@ -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')
}
}

View File

@@ -0,0 +1,9 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

View 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']
}
}

View File

@@ -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>

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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
}
}

View 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')
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View 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> }
}

View 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
}

View 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

View 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)
}

View 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)
}

View 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 '';
}
}

View 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

View File

@@ -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,
},

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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() : "未知错误";

View File

@@ -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);
// }
// }

View File

@@ -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();
}
}

Some files were not shown because too many files have changed in this diff Show More