Compare commits

...

3 Commits

Author SHA1 Message Date
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
75 changed files with 793 additions and 25481 deletions

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

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>

211
CLAUDE.md
View File

@@ -1,211 +0,0 @@
---
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个基于 **RuoYi-Vue 3.9.0** 的企业管理系统,集成了跨境电商 ERP 功能。项目包含:
* **RuoYi-Vue 核心**:基于 Spring Boot 2.5.15 后端和 Vue.js 2.6.12 前端的企业管理平台
* **ERP 客户端 (erp_client_sb)**:独立的跨境电商 ERP 桌面应用 (JavaFX + Spring Boot 3.5.4)
* **客户端监控扩展**实时监控、数据报表、API 集成等自定义功能
## 项目架构
### 主项目模块结构 (Maven 多模块)
```
ruoyi-admin/ # 主应用入口Web 控制器层
ruoyi-framework/ # 核心框架配置 (Spring Security, Redis, MyBatis)
ruoyi-system/ # 系统核心模块 (实体类、Mapper、Service)
ruoyi-common/ # 公共工具类和常量定义
ruoyi-quartz/ # 定时任务管理模块
ruoyi-generator/ # 代码生成器模块
ruoyi-ui/ # Vue.js 前端应用
erp_client_sb/ # 独立的跨境电商 ERP 客户端
```
### 技术栈详情
**后端 (Spring Boot 2.5.15)**
- Java 17 + Maven 3.11.0
- Spring Security 5.7.12 + JWT 0.9.1 认证
- MyBatis + PageHelper 1.4.7 分页
- MySQL + Redis 缓存
- Druid 1.2.23 连接池
- Swagger 3.0.0 API 文档
- FastJSON 2.0.57 JSON 处理
**前端 (Vue.js 2.6.12)**
- Element UI 2.15.14 组件库
- Vue Router 3.4.9 + Vuex 3.6.0
- Axios 0.28.1 HTTP 客户端
- ECharts 5.4.0 图表库
- Webpack 构建 (vue-cli-service 4.4.6)
**ERP 客户端 (JavaFX + Spring Boot 3.5.4)**
- OpenJFX 17.0.1 桌面 UI
- SQLite 3.42.0 本地数据库 + JPA/Hibernate
- WebMagic 1.0.3 + Selenium 4.23.0 网页爬取
- HutoolUtils 5.8.36 工具库
- 七牛云 7.12.1 存储服务
## 常用开发命令
### 后端开发 (Maven)
```bash
# 启动主应用 (推荐)
cd ruoyi-admin && mvn spring-boot:run
# 从根目录启动
mvn spring-boot:run -pl ruoyi-admin
# 打包部署
mvn clean package -DskipTests
# 运行测试
mvn test
# 编译项目
mvn clean compile
```
### 前端开发 (npm)
```bash
cd ruoyi-ui
# 安装依赖 (建议使用国内镜像)
npm install --registry=https://registry.npmmirror.com
# 启动开发服务器 (端口 80)
npm run dev
# 生产环境构建
npm run build:prod
# 测试环境构建
npm run build:stage
```
### ERP 客户端开发
```bash
cd erp_client_sb
# 启动 JavaFX 应用
mvn spring-boot:run
# 或者
mvn javafx:run
# 打包可执行 JAR
mvn clean package
```
## 核心配置与架构要点
### Maven 依赖管理
- 使用阿里云 Maven 镜像 (maven.aliyun.com)
- 父 POM 统一管理版本号和依赖
- 安全版本覆盖Tomcat 9.0.106, Logback 1.2.13, Spring Framework 5.3.39
### 前端开发配置
- **开发服务器**:端口 80支持热重载
- **代理配置**`vue.config.js` 中配置后端 API 代理 (`http://8.138.23.49:8080`)
- **构建优化**Gzip 压缩、代码分割、Element UI 单独打包
### 数据库与缓存
- **MySQL**:主数据库,通过 Druid 连接池管理
- **Redis**:会话存储、缓存、分布式锁
- **SQLite**ERP 客户端本地数据存储
### 安全与认证
- **JWT 无状态认证**:支持多终端
- **RBAC 权限模型**:角色-菜单-按钮权限
- **跨域配置**:支持前后端分离部署
- **XSS 防护**:输入过滤和输出编码
## 开发工作流
### 新功能开发标准流程
1. **后端开发**
-`ruoyi-system/src/main/java/com/ruoyi/system/domain/` 创建实体类
-`ruoyi-system/src/main/java/com/ruoyi/system/mapper/` 创建 Mapper 接口
-`ruoyi-system/src/main/resources/mapper/system/` 创建 MyBatis XML
-`ruoyi-system/src/main/java/com/ruoyi/system/service/` 实现业务逻辑
-`ruoyi-admin/src/main/java/com/ruoyi/web/controller/` 创建 REST 控制器
2. **前端开发**
-`ruoyi-ui/src/api/` 创建 API 服务模块
-`ruoyi-ui/src/views/` 创建页面组件
- 更新路由配置和菜单权限
3. **代码生成器**
- 访问 `/tool/gen` 快速生成 CRUD 代码
- 支持 Java、Vue、SQL 代码自动生成
### 客户端集成开发
ERP 客户端提供的核心功能:
- 多平台数据采集 (Amazon, Rakuten, Shopee, 1688)
- 实时状态监控与错误上报
- 与主系统的 API 集成 (`/monitor/client/api/**`)
## 重要配置文件
### 后端配置
- **主配置**`ruoyi-admin/src/main/resources/application.yml`
- **数据源配置**`ruoyi-admin/src/main/resources/application-druid.yml`
- **Maven 配置**:根目录 `pom.xml` (父 POM)
### 前端配置
- **构建配置**`ruoyi-ui/vue.config.js`
- **包管理**`ruoyi-ui/package.json`
### ERP 客户端配置
- **应用配置**`erp_client_sb/src/main/resources/application.yml`
- **Maven 配置**`erp_client_sb/pom.xml`
## 系统监控与工具
### 内置监控功能
- 系统性能监控:`/monitor/server`
- Redis 缓存监控:缓存信息查询和命令统计
- Druid 连接池监控SQL 性能分析
- 在线用户监控:当前活跃用户状态
- 操作日志:系统操作记录和异常日志
### API 文档
- Swagger UI`/swagger-ui/index.html`
- 自动生成的接口文档
## 部署和环境
### 开发环境要求
- **Java**JDK 17
- **Node.js**>= 8.9
- **npm**>= 3.0.0
- **MySQL**5.7+ 或 8.0+
- **Redis**6.0+
### 常见问题排查
- **端口冲突**:前端默认 80 端口,可在 `vue.config.js` 修改
- **跨域问题**:检查 `vue.config.js` 代理配置和后端 CORS 设置
- **Maven 依赖**:已配置阿里云镜像加速
- **JavaFX 运行**:确保 JDK 包含 JavaFX 模块或单独安装 OpenJFX
---
⚠️ **额外要求**:回答时必须使用中文。
💡 **操作提示**:在每次修改代码前,我会先向您说明修改的思路和方案,请您确认同意后再进行代码更改。
---

View File

@@ -57,26 +57,43 @@ function getJavaExecutablePath(): string {
return 'java';
}
function findJarFile(directory: string): string {
if (!existsSync(directory)) return '';
const files = require('fs').readdirSync(directory);
const jarFile = files.find((f: string) => f.startsWith('erp_client_sb-') && f.endsWith('.jar'));
return jarFile ? join(directory, jarFile) : '';
}
function extractVersionFromJar(jarPath: string): string {
if (!jarPath) return '';
const match = require('path').basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/);
return match?.[1] || '';
}
function getJarFilePath(): string {
if (process.env.NODE_ENV === 'development') {
return join(__dirname, '../../public/erp_client_sb-2.4.7.jar');
return findJarFile(join(__dirname, '../../public'));
}
// 生产环境需要将JAR包从asar提取到临时位置
const tempDir = join(app.getPath('temp'), 'erp-client');
const tempJarPath = join(tempDir, 'erp_client_sb-2.4.7.jar');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
// 确保临时目录存在
if (!existsSync(tempDir)) {
mkdirSync(tempDir, { recursive: true });
const asarJarPath = findJarFile(join(__dirname, '../assets'));
if (!asarJarPath) return '';
const asarFileName = require('path').basename(asarJarPath);
const tempJarPath = join(tempDir, asarFileName);
// 如果临时目录版本不同,删除旧版本并复制新版本
const existingJar = findJarFile(tempDir);
if (existingJar && require('path').basename(existingJar) !== asarFileName) {
require('fs').unlinkSync(existingJar);
}
// 如果临时JAR不存在从asar中复制
if (!existsSync(tempJarPath)) {
const asarJarPath = join(__dirname, '../assets/erp_client_sb-2.4.7.jar');
if (existsSync(asarJarPath)) {
copyFileSync(asarJarPath, tempJarPath);
}
copyFileSync(asarJarPath, tempJarPath);
}
return tempJarPath;
@@ -144,7 +161,6 @@ function migrateDataFromPublic(): void {
}
function startSpringBoot() {
// 首先迁移数据(如果需要)
migrateDataFromPublic();
const jarPath = getJarFilePath();
@@ -267,7 +283,6 @@ function createWindow() {
setTimeout(() => checkPendingUpdate(), 500);
});
// 不立即加载页面,等 SpringBoot 启动完成后再加载
}
app.whenReady().then(() => {
@@ -300,6 +315,10 @@ app.whenReady().then(() => {
splashWindow.loadFile(splashPath);
}
//11111
// setTimeout(() => {
// openAppIfNotOpened();
// }, 2000);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
@@ -321,6 +340,8 @@ ipcMain.on('message', (event, message) => {
console.log(message);
});
ipcMain.handle('get-jar-version', () => extractVersionFromJar(getJarFilePath()));
function checkPendingUpdate() {
try {
const updateFilePath = join(process.resourcesPath, 'app.asar.update');

View File

@@ -3,6 +3,8 @@ 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'),

View File

@@ -134,56 +134,43 @@ function handleMenuSelect(key: string) {
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
isAuthenticated.value = true
showAuthDialog.value = false
showRegDialog.value = false // 确保注册对话框也关闭
showRegDialog.value = false
try {
// 保存token到本地数据库
await authApi.saveToken(data.token)
const username = getUsernameFromToken(data.token)
currentUsername.value = username
userPermissions.value = data?.permissions || ''
await deviceApi.register({username})
// 建立SSE连接
SSEManager.connect()
} catch (e: any) {
// 设备注册失败时回滚登录状态
isAuthenticated.value = false
showAuthDialog.value = true
await authApi.deleteTokenCache()
ElMessage({
message: e?.message || '设备注册失败,请重试',
type: 'error'
})
ElMessage.error(e?.message || '设备注册失败')
}
}
async function logout() {
// 主动设置设备离线
try {
const deviceId = await getClientIdFromToken()
if (deviceId) {
await deviceApi.offline({ deviceId })
}
if (deviceId) await deviceApi.offline({ deviceId })
} catch (error) {
console.warn('离线通知失败:', error)
}
const token = await authApi.getToken()
if (token) {
await authApi.logout(token)
}
try {
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) await authApi.logout(token)
} catch {}
await authApi.deleteTokenCache()
// 清理前端状态
isAuthenticated.value = false
currentUsername.value = ''
userPermissions.value = ''
showAuthDialog.value = true
showDeviceDialog.value = false
// 关闭SSE连接
SSEManager.disconnect()
}
@@ -199,12 +186,8 @@ async function handleUserClick() {
cancelButtonText: '取消'
})
await logout()
ElMessage({
message: '已退出登录',
type: 'success'
})
} catch {
}
ElMessage.success('已退出登录')
} catch {}
}
function showRegisterDialog() {
@@ -218,26 +201,23 @@ function backToLogin() {
}
async function checkAuth() {
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
try {
await authApi.sessionBootstrap().catch(() => undefined)
const token = await authApi.getToken()
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) {
const response = await authApi.verifyToken(token)
if (response?.success) {
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
SSEManager.connect()
return
}
await authApi.deleteTokenCache()
await authApi.verifyToken(token)
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
SSEManager.connect()
return
}
} catch {
// 忽略
await authApi.deleteTokenCache()
}
if (authRequiredMenus.includes(activeMenu.value)) {
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
showAuthDialog.value = true
}
}
@@ -246,11 +226,11 @@ async function getClientIdFromToken(token?: string) {
try {
let t = token
if (!t) {
t = await authApi.getToken()
const tokenRes: any = await authApi.getToken()
t = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
}
if (!t) return ''
const payload = JSON.parse(atob(t.split('.')[1] || ''))
const payload = JSON.parse(atob(t.split('.')[1]))
return payload.clientId || ''
} catch {
return ''
@@ -259,7 +239,7 @@ async function getClientIdFromToken(token?: string) {
function getUsernameFromToken(token: string) {
try {
const payload = JSON.parse(atob(token.split('.')[1] || ''))
const payload = JSON.parse(atob(token.split('.')[1]))
return payload.username || ''
} catch {
return ''
@@ -273,21 +253,24 @@ const SSEManager = {
if (this.connection) return
try {
const token = await authApi.getToken()
if (!token) return
const tokenRes: any = await authApi.getToken()
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (!token) {
console.warn('SSE连接失败: 没有有效的 token')
return
}
const clientId = await getClientIdFromToken(token)
if (!clientId) return
if (!clientId) {
console.warn('SSE连接失败: 无法从 token 获取 clientId')
return
}
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
try {
const resp = await fetch('/api/config/server')
if (resp.ok) {
const config = await resp.json()
sseUrl = config.sseUrl || sseUrl
}
} catch {
}
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
} catch {}
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
this.connection = src
@@ -296,15 +279,13 @@ const SSEManager = {
src.onerror = () => this.handleError()
} catch (e: any) {
console.warn('SSE连接失败:', e?.message || e)
this.disconnect()
}
},
handleMessage(e: MessageEvent) {
try {
// 处理ping心跳
if (e.type === 'ping') {
return // ping消息自动保持连接无需处理
}
if (e.type === 'ping') return
console.log('SSE消息:', e.data)
const payload = JSON.parse(e.data)
@@ -314,17 +295,11 @@ const SSEManager = {
break
case 'DEVICE_REMOVED':
logout()
ElMessage({
message: '您的设备已被移除,请重新登录',
type: 'warning'
})
ElMessage.warning('您的设备已被移除,请重新登录')
break
case 'FORCE_LOGOUT':
logout()
ElMessage({
message: '会话已失效,请重新登录',
type: 'warning'
})
ElMessage.warning('会话已失效,请重新登录')
break
case 'PERMISSIONS_UPDATED':
checkAuth()
@@ -336,18 +311,16 @@ const SSEManager = {
},
handleError() {
this.disconnect()
setTimeout(() => this.connect(), 3000)
if (!this.connection) return
try { this.connection.close() } catch {}
this.connection = null
console.warn('SSE连接失败,已断开')
},
disconnect() {
if (this.connection) {
try {
this.connection.close()
} catch {
}
this.connection = null
}
if (!this.connection) return
try { this.connection.close() } catch {}
this.connection = null
},
}
@@ -366,26 +339,22 @@ function openSettings() {
async function fetchDeviceData() {
if (!currentUsername.value) {
ElMessage({
message: '未获取到用户名,请重新登录',
type: 'warning'
})
ElMessage.warning('未获取到用户名,请重新登录')
return
}
try {
deviceLoading.value = true
const [quota, list] = await Promise.all([
const [quotaRes, listRes] = await Promise.all([
deviceApi.getQuota(currentUsername.value),
deviceApi.list(currentUsername.value),
])
deviceQuota.value = quota || {limit: 0, used: 0}
]) as any[]
deviceQuota.value = quotaRes?.data || quotaRes || {limit: 0, used: 0}
const clientId = await getClientIdFromToken()
devices.value = (list || []).map(d => ({...d, isCurrent: d.deviceId === clientId})) as any
const list = listRes?.data || listRes || []
devices.value = list.map(d => ({...d, isCurrent: d.deviceId === clientId}))
} catch (e: any) {
ElMessage({
message: e?.message || '获取设备列表失败',
type: 'error'
})
ElMessage.error(e?.message || '获取设备列表失败')
} finally {
deviceLoading.value = false
}
@@ -403,21 +372,12 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
// 如果是本机设备被移除执行logout
const clientId = await getClientIdFromToken()
if (row.deviceId === clientId) {
await logout()
}
if (row.deviceId === clientId) await logout()
ElMessage({
message: '已移除设备',
type: 'success'
})
ElMessage.success('已移除设备')
} catch (e: any) {
ElMessage({
message: '移除设备失败: ' + ((e as any)?.message || '未知错误'),
type: 'error'
})
if (e !== 'cancel') ElMessage.error('移除设备失败: ' + (e?.message || '未知错误'))
}
}

View File

@@ -8,8 +8,8 @@ export const amazonApi = {
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) {
return http.post<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/batch', { asinList, batchId, region });
},
getLatestProducts() {
return http.get<{ code: number, data: { products: any[] }, msg: string | null }>('/api/amazon/products/latest');

View File

@@ -1,113 +1,39 @@
import { http } from './http';
// 统一响应处理函数 - 适配ERP客户端格式
function unwrap<T>(res: any): T {
if (res && typeof res.success === 'boolean') {
if (!res.success) {
const message: string = res.message || res.msg || '请求失败';
throw new Error(message);
}
return res as T;
}
// 兼容标准格式
if (res && typeof res.code === 'number') {
if (res.code !== 0) {
const message: string = res.msg || '请求失败';
throw new Error(message);
}
return (res.data as T) ?? ({} as T);
}
return res as T;
}
// 认证相关类型定义
interface LoginRequest {
username: string;
password: string;
}
interface RegisterRequest {
username: string;
password: string;
}
interface LoginResponse {
success: boolean;
token: string;
permissions: string[];
username: string;
message?: string;
}
interface RegisterResponse {
success: boolean;
message?: string;
}
import { http } from './http'
export const authApi = {
// 用户登录
login(params: LoginRequest) {
return http
.post('/api/login', params)
.then(res => unwrap<LoginResponse>(res));
login(params: { username: string; password: string }) {
return http.post('/api/login', params)
},
// 用户注册
register(params: RegisterRequest) {
return http
.post('/api/register', params)
.then(res => unwrap<RegisterResponse>(res));
register(params: { username: string; password: string }) {
return http.post('/api/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('/api/check-username', { username })
},
// 验证token有效性
verifyToken(token: string) {
return http
.post('/api/verify', { token })
.then(res => unwrap<{ success: boolean }>(res));
return http.post('/api/verify', { token })
},
// 用户登出
logout(token: string) {
return http.postVoid('/api/logout', { token });
return http.postVoid('/api/logout', { token })
},
// 删除token缓存
deleteTokenCache() {
return http.postVoid('/api/cache/delete?key=token');
return http.postVoid('/api/cache/delete?key=token')
},
// 保存token到本地数据库
saveToken(token: string) {
return http.postVoid('/api/cache/save', { key: 'token', value: token });
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;
});
getToken() {
return http.get('/api/cache/get?key=token')
},
// 会话引导:检查并恢复会话(返回体各异,这里保持 any
sessionBootstrap() {
return http.get<any>('/api/session/bootstrap');
},
};
return http.get('/api/session/bootstrap')
}
}

View File

@@ -1,52 +1,27 @@
import { http } from './http'
// 与老版保持相同的接口路径与参数
const base = '/api/device'
export interface DeviceQuota {
limit: number
used: number
}
export interface DeviceItem {
deviceId: string
name?: string
status?: 'online' | 'offline'
lastActiveAt?: string
}
// 统一处理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('/api/device/quota', { username })
},
list(username: string): Promise<DeviceItem[]> {
return http.get(`${base}/list`, { username }).then(handleAjaxResult)
list(username: string) {
return http.get('/api/device/list', { username })
},
register(payload: { username: string }) {
return http.post(`${base}/register`, payload).then(handleAjaxResult)
return http.post('/api/device/register', payload)
},
remove(payload: { deviceId: string }) {
return http.post(`${base}/remove`, payload).then(handleAjaxResult)
return http.post('/api/device/remove', payload)
},
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
return http.post(`${base}/heartbeat`, payload).then(handleAjaxResult)
return http.post('/api/device/heartbeat', payload)
},
offline(payload: { deviceId: string }) {
return http.post(`${base}/offline`, payload).then(handleAjaxResult)
},
}
return http.post('/api/device/offline', payload)
}
}

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));
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)
},
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));
const payload: Record<string, unknown> = { imageUrl }
if (sessionId) payload.sessionId = sessionId
return http.post('/api/rakuten/search1688', payload)
},
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,11 @@
import { http } from './http'
export const updateApi = {
getVersion() {
return http.get('/api/update/version')
},
checkUpdate(currentVersion: string) {
return http.get(`/system/version/check?currentVersion=${currentVersion}`)
}
}

View File

@@ -1,79 +1,39 @@
// 斑马订单模型(根据页面所需字段精简定义)
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');
return http.get('/tool/banma/accounts')
},
saveAccount(body: BanmaAccount) {
return http.post<{ id: number }>('/tool/banma/accounts', body);
saveAccount(body: any) {
return http.post('/tool/banma/accounts', 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>)
},
getOrders(params: any) {
return http.get('/api/banma/orders', params as Record<string, unknown>)
},
// 其他功能(客户端微服务)
getOrdersByBatch(batchId: string) {
return http.get<ZebraOrdersResp>(`/api/banma/orders/batch/${batchId}`);
return http.get(`/api/banma/orders/batch/${batchId}`)
},
getLatestOrders() {
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
return http.get('/api/banma/orders/latest')
},
getOrderStats() {
return http.get('/api/banma/orders/stats');
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/search', searchParams)
}
}

View File

@@ -32,7 +32,6 @@ const region = ref('JP')
const regionOptions = [
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
{ label: '中国 (China)', value: 'CN', flag: '🇨🇳' },
]
const pendingAsins = ref<string[]>([])
@@ -106,7 +105,7 @@ async function batchGetProductInfo(asinList: string[]) {
currentAsin.value = `正在处理第${i + 1}/${totalBatches}批 (${batchAsins.join(', ')})`
try {
const result = await amazonApi.getProductsBatch(batchAsins, batchId)
const result = await amazonApi.getProductsBatch(batchAsins, batchId, region.value)
if (result?.data?.products?.length > 0) {
localProductData.value.push(...result.data.products)
@@ -296,6 +295,17 @@ onMounted(async () => {
<div class="body-layout">
<!-- 左侧步骤栏 -->
<aside class="steps-sidebar">
<!-- 顶部标签栏 -->
<div class="top-tabs">
<div class="tab-item active">
<span class="tab-icon">📦</span>
<span class="tab-text">ASIN查询</span>
</div>
<div class="tab-item" @click="openGenmaiSpirit">
<span class="tab-icon">🔍</span>
<span class="tab-text">跟卖精灵</span>
</div>
</div>
<div class="steps-title">操作流程</div>
<div class="steps-flow">
<!-- 1 -->
@@ -357,7 +367,6 @@ onMounted(async () => {
</div>
<div class="export-progress-text">{{ Math.round(exportProgress) }}%</div>
</div>
<el-button size="small" class="w100 btn-blue" :loading="genmaiLoading" @click="openGenmaiSpirit">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</el-button>
</div>
</div>
</div>
@@ -454,14 +463,25 @@ onMounted(async () => {
<style scoped>
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
.body-layout { display: flex; gap: 12px; height: 100%; }
/* 顶部标签栏 */
.top-tabs { display: flex; margin-bottom: 8px; }
.tab-item { flex: 1; display: flex; align-items: center; justify-content: center; gap: 3px; padding: 4px 6px; cursor: pointer; transition: all 0.2s ease; background: #f5f7fa; color: #606266; font-size: 11px; font-weight: 500; border: 1px solid #ebeef5; }
.tab-item:first-child { border-radius: 3px 0 0 3px; }
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
.tab-icon { font-size: 12px; }
.tab-text { line-height: 1; }
.body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; }
.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; }
.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; }
.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 + .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 .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; }
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
@@ -522,14 +542,14 @@ onMounted(async () => {
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: center; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; text-align: center; }
.table tbody tr:hover { background: #f9f9f9; }
.table th:nth-child(1), .table td:nth-child(1) { width: 33.33%; }
.table th:nth-child(2), .table td:nth-child(2) { width: 33.33%; }
.table th:nth-child(3), .table td:nth-child(3) { width: 33.33%; }
.asin-out { color: #f56c6c; font-weight: 600; }
.seller-info { display: flex; align-items: center; gap: 4px; }
.seller-info { display: flex; align-items: center; gap: 4px; justify-content: center; }
.seller { color: #303133; font-weight: 500; }
.shipper { color: #909399; font-size: 12px; }
.price { color: #e6a23c; font-weight: 600; }

View File

@@ -31,11 +31,10 @@ async function handleAuth() {
authLoading.value = true
try {
// 1. 先检查设备限制
await deviceApi.register({ username: authForm.value.username })
// 2. 设备检查通过,进行登录
const data = await authApi.login(authForm.value)
const loginRes: any = await authApi.login(authForm.value)
const data = loginRes?.data || loginRes
emit('loginSuccess', {
token: data.token,
user: {

View File

@@ -41,8 +41,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)
const data = res?.data || res
usernameCheckResult.value = data?.available || false
} catch {
usernameCheckResult.value = null
}
@@ -53,18 +54,17 @@ async function handleRegister() {
registerLoading.value = true
try {
// 1. 注册
await authApi.register({
username: registerForm.value.username,
password: registerForm.value.password
})
// 2. 注册成功后直接登录
const loginData = await authApi.login({
const loginRes: any = await authApi.login({
username: registerForm.value.username,
password: registerForm.value.password
})
const loginData = loginRes?.data || loginRes
emit('loginSuccess', {
token: loginData.token,
user: {

View File

@@ -0,0 +1,332 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
getSettings,
saveSettings,
savePlatformSettings,
type Platform,
type PlatformExportSettings
} from '../../utils/settings'
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 平台配置
const platforms = [
{ key: 'amazon' as Platform, name: 'Amazon', icon: '🛒', color: '#FF9900' },
{ key: 'rakuten' as Platform, name: 'Rakuten', icon: '🛍️', color: '#BF0000' },
{ key: 'zebra' as Platform, name: 'Zebra', icon: '🦓', color: '#34495E' }
]
// 设置项
const platformSettings = ref<Record<Platform, PlatformExportSettings>>({
amazon: { exportPath: '' },
rakuten: { exportPath: '' },
zebra: { exportPath: '' }
})
const activeTab = ref<Platform>('amazon')
const show = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 选择导出路径
async function selectExportPath(platform: Platform) {
const result = await (window as any).electronAPI.showOpenDialog({
title: `选择${platforms.find(p => p.key === platform)?.name}默认导出路径`,
properties: ['openDirectory'],
defaultPath: platformSettings.value[platform].exportPath
})
if (!result.canceled && result.filePaths?.length > 0) {
platformSettings.value[platform].exportPath = result.filePaths[0]
}
}
// 保存设置
function saveAllSettings() {
Object.keys(platformSettings.value).forEach(platformKey => {
const platform = platformKey as Platform
const platformConfig = platformSettings.value[platform]
savePlatformSettings(platform, platformConfig)
})
ElMessage({ message: '设置已保存', type: 'success' })
show.value = false
}
// 加载设置
function loadAllSettings() {
const settings = getSettings()
platformSettings.value = {
amazon: { ...settings.platforms.amazon },
rakuten: { ...settings.platforms.rakuten },
zebra: { ...settings.platforms.zebra }
}
}
// 重置单个平台设置
function resetPlatformSettings(platform: Platform) {
platformSettings.value[platform] = {
exportPath: ''
}
}
// 重置所有设置
function resetAllSettings() {
platforms.forEach(platform => {
resetPlatformSettings(platform.key)
})
}
onMounted(() => {
loadAllSettings()
})
</script>
<template>
<el-dialog
v-model="show"
title="应用设置"
width="480px"
:close-on-click-modal="false"
class="settings-dialog">
<div class="settings-content">
<!-- 平台选择标签 -->
<div class="platform-tabs">
<div
v-for="platform in platforms"
:key="platform.key"
:class="['platform-tab', { active: activeTab === platform.key }]"
@click="activeTab = platform.key"
:style="{ '--platform-color': platform.color }">
<span class="platform-icon">{{ platform.icon }}</span>
<span class="platform-name">{{ platform.name }}</span>
</div>
</div>
<!-- 当前平台设置 -->
<div class="setting-section">
<div class="section-title">
<span class="title-icon">📁</span>
<span>{{ platforms.find(p => p.key === activeTab)?.name }} 导出设置</span>
</div>
<div class="setting-item">
<div class="setting-label">默认导出路径</div>
<div class="setting-desc">设置 {{ platforms.find(p => p.key === activeTab)?.name }} Excel文件的默认保存位置</div>
<div class="path-input-group">
<el-input
v-model="platformSettings[activeTab].exportPath"
placeholder="留空时自动弹出保存对话框"
readonly
class="path-input">
<template #suffix>
<el-button
size="small"
type="primary"
@click="selectExportPath(activeTab)"
class="select-btn">
浏览
</el-button>
</template>
</el-input>
</div>
</div>
<div class="setting-actions">
<el-button
size="small"
@click="resetPlatformSettings(activeTab)">
重置此平台
</el-button>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="resetAllSettings">重置全部</el-button>
<el-button @click="show = false">取消</el-button>
<el-button type="primary" @click="saveAllSettings">保存设置</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.settings-dialog :deep(.el-dialog__body) {
padding: 0 20px 20px 20px;
}
.settings-content {
max-height: 500px;
overflow-y: auto;
}
.platform-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
padding: 4px;
background: #F8F9FA;
border-radius: 8px;
}
.platform-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
background: transparent;
color: #606266;
font-size: 13px;
}
.platform-tab:hover {
background: rgba(255, 255, 255, 0.8);
color: var(--platform-color);
}
.platform-tab.active {
background: #fff;
color: var(--platform-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-weight: 500;
}
.platform-icon {
font-size: 16px;
}
.platform-name {
font-size: 12px;
}
.setting-section {
margin-bottom: 24px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #EBEEF5;
}
.title-icon {
font-size: 18px;
}
.setting-item {
margin-bottom: 20px;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-info {
flex: 1;
}
.setting-label {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.setting-desc {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.path-input-group {
margin-top: 8px;
}
.path-input {
width: 100%;
}
.path-input :deep(.el-input__wrapper) {
padding-right: 80px;
}
.select-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
height: 24px;
padding: 0 12px;
font-size: 12px;
}
.info-content {
background: #F8F9FA;
border-radius: 6px;
padding: 12px;
font-size: 13px;
color: #606266;
line-height: 1.5;
}
.info-content p {
margin: 0 0 6px 0;
}
.info-content p:last-child {
margin-bottom: 0;
}
.setting-actions {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #EBEEF5;
}
.settings-dialog :deep(.el-dialog__header) {
text-align: center;
padding-right: 40px; /* 为右侧关闭按钮留出空间 */
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>
<script lang="ts">
export default {
name: 'SettingsDialog'
}
</script>

View File

@@ -2,15 +2,18 @@
<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"
: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/icon.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>
@@ -20,7 +23,7 @@
class="notes-box"
:rows="6"
readonly
resize="none" />
resize="none"/>
</div>
<div class="update-actions row">
@@ -41,7 +44,7 @@
<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/icon.png" class="app-icon" alt="App Icon"/>
</div>
<div class="download-content">
<div class="download-info">
@@ -52,7 +55,7 @@
: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>
@@ -64,7 +67,7 @@
<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" />
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
<h3>更新完成</h3>
<p>更新文件已下载将在重启后自动应用</p>
</div>
@@ -77,7 +80,7 @@
:percentage="100"
:show-text="false"
:stroke-width="6"
color="#67C23A" />
color="#67C23A"/>
</div>
<div class="update-buttons">
@@ -90,9 +93,9 @@
</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} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {updateApi} from '../../api/update'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
@@ -105,8 +108,8 @@ 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', speed: ''})
const info = ref({
latestVersion: '2.4.8',
downloadUrl: '',
@@ -117,49 +120,48 @@ const info = ref({
async function autoCheck() {
try {
ElMessage({ message: '正在检查更新...', type: 'info' })
version.value = await (window as any).electronAPI.getJarVersion()
const checkRes: any = await updateApi.checkUpdate(version.value)
const result = checkRes?.data || checkRes
try {
version.value = await updateApi.getVersion()
} catch (error) {
console.error('获取版本失败:', error)
version.value = '2.0.0'
if (!result.needUpdate) {
ElMessage.info('当前已是最新版本')
return
}
info.value = {
currentVersion: version.value,
latestVersion: '2.4.9',
downloadUrl: 'https://qiniu.pxdj.tashowz.com/2025/09/becac13811214c909d11162d2ff2c863.asar',
currentVersion: result.currentVersion,
latestVersion: result.latestVersion,
downloadUrl: result.downloadUrl || '',
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 轻量级更新仅替换app.asar',
hasUpdate: true
}
show.value = true
stage.value = 'check'
ElMessage({ message: '发现新版本', type: 'success' })
ElMessage.success('发现新版本')
} catch (error) {
console.error('检查更新失败:', error)
ElMessage({ message: '检查更新失败', type: 'error' })
ElMessage.error('检查更新失败')
}
}
async function start() {
if (!info.value.downloadUrl) {
ElMessage({ message: '下载链接不可用', type: 'error' });
return;
ElMessage.error('下载链接不可用')
return
}
stage.value = 'downloading';
prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' };
stage.value = 'downloading'
prog.value = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}
(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 || ''
};
});
}
})
try {
const response = await (window as any).electronAPI.downloadUpdate(info.value.downloadUrl)
@@ -167,14 +169,14 @@ async function start() {
if (response.success) {
stage.value = 'completed'
prog.value.percentage = 100
ElMessage({ message: '下载完成', type: 'success' })
ElMessage.success('下载完成')
} else {
ElMessage({ message: '下载失败: ' + (response.error || '未知错误'), type: 'error' })
ElMessage.error('下载失败: ' + (response.error || '未知错误'))
stage.value = 'check'
}
} catch (error) {
console.error('下载失败:', error)
ElMessage({ message: '下载失败', type: 'error' })
ElMessage.error('下载失败')
stage.value = 'check'
}
}
@@ -195,37 +197,26 @@ async function cancelDownload() {
async function installUpdate() {
try {
await ElMessageBox.confirm(
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
'确认安装',
{
confirmButtonText: '立即安装',
cancelButtonText: '取消',
type: 'warning'
}
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
'确认安装',
{
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('安装失败')
}
}
onMounted(async () => {
try {
version.value = await updateApi.getVersion()
} catch (error) {
console.error('获取版本失败:', error)
}
version.value = await (window as any).electronAPI.getJarVersion()
})
onUnmounted(() => {
@@ -238,7 +229,7 @@ onUnmounted(() => {
position: fixed;
right: 10px;
bottom: 10px;
background: rgba(255,255,255,0.9);
background: rgba(255, 255, 255, 0.9);
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
@@ -273,11 +264,38 @@ onUnmounted(() => {
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; }
.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;
@@ -324,8 +342,13 @@ onUnmounted(() => {
margin: 12px 0 8px 0;
}
.update-details.form { max-height: none; }
.notes-box :deep(textarea.el-textarea__inner) { white-space: pre-wrap; }
.update-details.form {
max-height: none;
}
.notes-box :deep(textarea.el-textarea__inner) {
white-space: pre-wrap;
}
.update-details h4 {
font-size: 14px;
@@ -347,10 +370,24 @@ onUnmounted(() => {
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; }
.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;

View File

@@ -49,9 +49,7 @@ 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)
@@ -432,7 +430,7 @@ onMounted(loadLatest)
<div class="title">网站地区</div>
</div>
<div class="desc">请选择目标网站地区日本区</div>
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
<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>
@@ -602,14 +600,14 @@ onMounted(loadLatest)
.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; }
.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; }
.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 + .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 .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; }
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }

View File

@@ -538,12 +538,12 @@ export default {
.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 { display: grid; grid-template-columns: 22px 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-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: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
.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; }

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId>
<version>2.4.7</version>
<version>2.4.8</version>
<name>erp_client_sb</name>
<description>erp客户端</description>
<properties>

View File

@@ -13,23 +13,15 @@ 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());
}
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");
ResourcePreloader.init();
ResourcePreloader.preloadErpDashboard();
ResourcePreloader.executePreloading();
try {
ResourcePreloader.init();
ResourcePreloader.preloadErpDashboard();
ResourcePreloader.executePreloading();
} catch (Throwable t) {
log.warn("资源预加载失败: {}", t.getMessage());
}
}
}

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,63 +0,0 @@
// package com.tashow.erp.config;
//
// 已转为纯 Spring BootJavaFX 启动屏幕不再使用。如需恢复,可取消注释并补充 JavaFX 依赖。
//
// import javafx.application.Platform;
// import javafx.scene.Scene;
// import javafx.scene.control.ProgressBar;
// import javafx.scene.image.Image;
// import javafx.scene.image.ImageView;
// import javafx.scene.layout.StackPane;
// import javafx.scene.layout.VBox;
// import javafx.stage.Stage;
// import javafx.stage.StageStyle;
// import lombok.extern.slf4j.Slf4j;
// import org.springframework.stereotype.Component;
//
// /**
// * 自定义启动屏幕
// */
// @Slf4j
// @Component
// public class MySplashScreen {
// private static final String DEFAULT_IMAGE = "/static/image/splash_screen.png";
// private Stage splashStage;
// private ProgressBar progressBar;
//
// public void show() {
// Platform.runLater(() -> {
// try {
// splashStage = new Stage();
// splashStage.initStyle(StageStyle.UNDECORATED);
// Image splashImage = new Image(getClass().getResourceAsStream(DEFAULT_IMAGE));
// ImageView imageView = new ImageView(splashImage);
// ProgressBar progressBar = new ProgressBar();
// progressBar.setPrefWidth(splashImage.getWidth());
// progressBar.setMaxWidth(Double.MAX_VALUE);
// progressBar.setStyle("-fx-accent: #0078d4; -fx-background-color: transparent;");
// StackPane root = new StackPane();
// VBox progressContainer = new VBox();
// progressContainer.setAlignment(javafx.geometry.Pos.BOTTOM_CENTER);
// progressContainer.setSpacing(0);
// progressContainer.getChildren().add(progressBar);
// root.getChildren().addAll(imageView, progressContainer);
// Scene scene = new Scene(root);
// splashStage.setScene(scene);
// splashStage.setResizable(false);
// splashStage.centerOnScreen();
// splashStage.show();
// } catch (Exception e) {
// log.error("显示启动屏幕失败", e);
// }
// });
// }
//
// public void hide() {
// Platform.runLater(() -> {
// if (splashStage != null) {
// splashStage.hide();
// log.info("启动屏幕已隐藏");
// }
// });
// }
// }

View File

@@ -36,7 +36,8 @@ public class AmazonController {
Map<String, Object> requestMap = (Map<String, Object>) request;
List<String> asinList = (List<String>) requestMap.get("asinList");
String batchId = (String) requestMap.get("batchId");
List<AmazonProductEntity> products = amazonScrapingService.batchGetProductInfo(asinList, batchId);
String region = (String) requestMap.getOrDefault("region", "JP");
List<AmazonProductEntity> products = amazonScrapingService.batchGetProductInfo(asinList, batchId, region);
Map<String, Object> result = new HashMap<>();
result.put("products", products);
result.put("total", products.size());

View File

@@ -21,6 +21,9 @@ public class AmazonProductEntity {
@Column
private String asin;
@Column(name = "region", length = 10)
private String region; // 地区代码(JP/US)
@Column(name = "price")
private String price;

View File

@@ -1,43 +0,0 @@
// package com.tashow.erp.fx.controller;
//
// 已转为纯 Spring Boot不再包含 JavaFX 控制器与视图逻辑。如需恢复 FX请取消注释并恢复依赖。
//
// import javafx.application.Platform;
// import javafx.fxml.FXML;
// import javafx.fxml.Initializable;
// import javafx.scene.layout.BorderPane;
// import javafx.scene.web.WebEngine;
// import javafx.scene.web.WebView;
// import lombok.extern.slf4j.Slf4j;
// import net.rgielen.fxweaver.core.FxmlView;
// import org.springframework.stereotype.Component;
// import netscape.javascript.JSObject;
//
// import java.net.URL;
// import java.util.ResourceBundle;
//
// @Slf4j
// @Component
// @FxmlView("/static/fxml/Main.fxml")
// public class MainCtrl implements Initializable {
// @FXML
// public BorderPane rootPane;
// @FXML
// public WebView webView;
// private WebEngine webEngine;
// @Override
// public void initialize(URL location, ResourceBundle resources) {
// if (Platform.isFxApplicationThread()) {
// initWebView();
// } else {
// Platform.runLater(this::initWebView);
// }
// }
//
// private void initWebView() {
// webEngine = webView.getEngine();
// webEngine.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...");
// webEngine.setJavaScriptEnabled(true);
// webEngine.load("http://localhost:8081/html/erp-dashboard.html");
// }
// }

View File

@@ -1,11 +0,0 @@
// package com.tashow.erp.fx.view;
//
// 已转为纯 Spring Boot移除 FX 视图。
// import javafx.scene.Parent;
// import net.rgielen.fxweaver.core.FxmlView;
// import org.springframework.stereotype.Component;
//
// @Component
// @FxmlView("/static/fxml/Main.fxml")
// public class MainView {
// }

View File

@@ -21,10 +21,10 @@ import java.util.Optional;
public interface AmazonProductRepository extends JpaRepository<AmazonProductEntity, Long> {
/**
* 根据ASIN查找产品取最新的一条
* 根据ASIN和地区查找产品(取最新的一条)
*/
@Query(value = "SELECT * FROM amazon_products WHERE asin = :asin ORDER BY created_at DESC LIMIT 1", nativeQuery = true)
Optional<AmazonProductEntity> findByAsin(@Param("asin") String asin);
@Query(value = "SELECT * FROM amazon_products WHERE asin = :asin AND region = :region ORDER BY created_at DESC LIMIT 1", nativeQuery = true)
Optional<AmazonProductEntity> findByAsinAndRegion(@Param("asin") String asin, @Param("region") String region);
/**
* 根据会话ID查找产品分页

View File

@@ -26,7 +26,7 @@ public class LocalJwtAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if (uri.startsWith("/libs/") || uri.startsWith("/static/") || uri.startsWith("/favicon")
if (uri.startsWith("/libs/") || uri.startsWith("/favicon")
|| uri.startsWith("/api/cache") || uri.startsWith("/api/update")) {
return true;
}

View File

@@ -15,9 +15,10 @@ public interface IAmazonScrapingService {
*
* @param asinList ASIN列表
* @param batchId 批次ID
* @param region 地区代码 (JP/US)
* @return 产品信息列表
*/
List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId);
List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId, String region);

View File

@@ -286,42 +286,4 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service {
}
// private String getWeight(List<String> ids) {
// Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:weight|unitWeight)\":\\s*([1-9]\\d*(?:\\.\\d+)?)");
// List<String> weightList = new ArrayList<>();
// ChromeDriver driver = driverManager.getCurrentDriver();
// Set<String> weightSet = new HashSet<>();
// try {
// for (int i = 0; weightSet.size() <= 2; i++) {
// driver.get("https://detail.1688.com/offer/" + ids.get(i) + ".html");
// if(i==0) driver.navigate().refresh();
// if (Objects.equals(driver.getTitle(), "验证码拦截")) {
// driver = driverManager.switchToHeadful();
// while (Objects.equals(driver.getTitle(), "验证码拦截")) {
// Thread.sleep(1000);
// }
// }else if(Objects.equals(driver.getTitle(), "淘宝网 - 淘!我喜欢")){
//
// }
// System.out.println("标题"+driver.getTitle());
// String source = driver.getPageSource();
// Matcher weightMatcher = WEIGHT_PATTERN.matcher(source);
// if (weightMatcher.find()) {
// String weightValue = weightMatcher.group(1);
// String width = weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g";
// weightSet.add(width);
// System.out.println("重量"+width);
// }
// Thread.sleep(2000+random.nextInt(3000));
// }
// } catch (InterruptedException e) {
// e.printStackTrace();
// errorReporter.reportDataCollectError("获取重量出错",e);
// }
// weightList.addAll(weightSet);
// weightList.sort((a, b) -> Integer.compare(Integer.parseInt(a.replace("g", "")), Integer.parseInt(b.replace("g", ""))));
// System.out.println("weightList: " +ids+"::::::"+ weightList);
// return weightList.get(1);
// }
}

View File

@@ -38,7 +38,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
private static volatile Spider activeSpider = null;
private static final Object spiderLock = new Object();
private final Map<String, AmazonProductEntity> resultCache = new ConcurrentHashMap<>();
private final Site site = Site.me().setRetryTimes(3).setSleepTime(2000 + random.nextInt(2000)).setTimeOut(15000).setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/128.0.0.0 Safari/537.36").addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9").addHeader("accept-language", "ja,en;q=0.9,zh-CN;q=0.8,zh;q=0.7").addHeader("cache-control", "max-age=0").addHeader("upgrade-insecure-requests", "1").addHeader("sec-ch-ua", "\"Chromium\";v=\"128\", \"Not=A?Brand\";v=\"24\"").addHeader("sec-ch-ua-mobile", "?0").addHeader("sec-ch-ua-platform", "\"Windows\"").addHeader("sec-fetch-site", "none").addHeader("sec-fetch-mode", "navigate").addHeader("sec-fetch-user", "?1").addHeader("sec-fetch-dest", "document").addCookie("i18n-prefs", "JPY").addCookie("session-id", "358-1261309-0483141").addCookie("session-id-time", "2082787201l").addCookie("i18n-prefs", "JPY").addCookie("lc-acbjp", "zh_CN").addCookie("ubid-acbjp", "357-8224002-9668932");
private Site site;
/**
* 处理亚马逊页面数据提取
@@ -48,6 +48,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
Html html = page.getHtml();
String url = page.getUrl().toString();
// 提取ASIN
String asin = html.xpath("//input[@id='ASIN']/@value").toString();
if (isEmpty(asin)) {
@@ -76,10 +77,8 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
throw new RuntimeException("Retry this page");
}
// 检查并上报空数据
if (isEmpty(price)) errorReporter.reportDataEmpty("amazon", asin, price);
if (isEmpty(seller)) errorReporter.reportDataEmpty("amazon", asin, seller);
AmazonProductEntity entity = new AmazonProductEntity();
entity.setAsin(asin != null ? asin : "");
entity.setPrice(price);
@@ -101,7 +100,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
* 批量获取产品信息
*/
@Override
public List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId) {
public List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId, String region) {
String sessionId = (batchId != null) ? batchId : "SINGLE_" + UUID.randomUUID();
LocalDateTime batchTime = LocalDateTime.now(); // 统一的批次时间
@@ -114,7 +113,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
if (asin == null || asin.trim().isEmpty()) continue;
String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", "");
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsin(cleanAsin);
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsinAndRegion(cleanAsin, region);
if (cached.isPresent() && !isEmpty(cached.get().getPrice()) && !isEmpty(cached.get().getSeller())) {
AmazonProductEntity entity = cached.get();
entity.setSessionId(sessionId);
@@ -122,7 +121,8 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
amazonProductRepository.save(entity);
allProducts.put(cleanAsin, entity);
} else {
String url = "https://www.amazon.co.jp/dp/" + cleanAsin;
String url = buildAmazonUrl(region, cleanAsin);
this.site = configureSiteForRegion(region);
RakutenProxyUtil proxyUtil = new RakutenProxyUtil();
synchronized (spiderLock) {
activeSpider = Spider.create(this).addUrl(url).setDownloader(proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(url))).thread(1);
@@ -131,6 +131,7 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
}
AmazonProductEntity entity = resultCache.getOrDefault(cleanAsin, new AmazonProductEntity());
entity.setAsin(cleanAsin);
entity.setRegion(region);
entity.setSessionId(sessionId);
entity.setUpdatedAt(LocalDateTime.now());
amazonProductRepository.save(entity);
@@ -146,4 +147,41 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
return str == null || str.trim().isEmpty();
}
/**
* 根据地区构建Amazon URL
*/
private String buildAmazonUrl(String region, String asin) {
if ("US".equals(region)) {
return "https://www.amazon.com/dp/" + asin;
}
return "https://www.amazon.co.jp/dp/" + asin;
}
/**
* 根据地区配置Site
*/
private Site configureSiteForRegion(String region) {
boolean isUS = "US".equals(region);
return Site.me()
.setRetryTimes(3)
.setSleepTime(3000 + random.nextInt(3000))
.setTimeOut(20000)
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
.addHeader("accept-encoding", "gzip, deflate, br")
.addHeader("accept-language", isUS ? "zh-CN,zh;q=0.9,en;q=0.8" : "ja,en;q=0.9,zh-CN;q=0.8")
.addHeader("cache-control", "max-age=0")
.addHeader("referer", isUS ? "https://www.amazon.com/" : "https://www.amazon.co.jp/")
.addHeader("sec-fetch-site", "none")
.addHeader("sec-fetch-mode", "navigate")
.addHeader("sec-fetch-user", "?1")
.addHeader("sec-fetch-dest", "document")
.addCookie("i18n-prefs", isUS ? "USD" : "JPY")
.addCookie(isUS ? "lc-main" : "lc-acbjp", isUS ? "en_US" : "zh_CN")
.addCookie("session-id", isUS ? "134-6097934-2082600" : "358-1261309-0483141")
.addCookie("session-id-time", "2082787201l")
.addCookie(isUS ? "ubid-main" : "ubid-acbjp", isUS ? "132-7547587-3056927" : "357-8224002-9668932")
.addCookie("skin", "noskin");
}
}

View File

@@ -34,9 +34,6 @@ spring:
connection:
provider_disables_autocommit: true
open-in-view: false
web:
resources:
static-locations: classpath:/static/
server:
port: 8081
address: 127.0.0.1

View File

@@ -1,3 +0,0 @@
.root {
/*-fx-background-color: #f0f0f0;*/
}

View File

@@ -1,709 +0,0 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
}
/* 主容器 */
.erp-container {
display: flex;
height: 100vh;
overflow: hidden;
}
/* 左侧导航栏 */
.sidebar {
width: 200px;
background-color: #ffffff;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.user-avatar {
padding: 20px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
.menu-group-title {
padding: 15px 20px 8px;
font-size: 12px;
color: #999;
font-weight: 500;
}
.sidebar-menu {
border: none;
flex: 1;
}
.menu-item-custom {
height: 45px;
line-height: 45px;
margin: 0 10px;
border-radius: 6px;
}
.menu-item-custom:hover {
background-color: #f5f7fa !important;
}
.menu-item-custom.is-active {
background-color: #ecf5ff !important;
color: #409EFF !important;
}
.platform-item {
display: flex;
align-items: center;
}
.platform-logo {
width: 20px;
height: 20px;
margin-right: 8px;
border-radius: 4px;
}
.vip-section {
padding: 20px;
border-top: 1px solid #f0f0f0;
}
.vip-button {
width: 100%;
height: 60px;
background: linear-gradient(135deg, #FFD700, #FFA500);
border: none;
border-radius: 8px;
color: white;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.vip-button:hover {
background: linear-gradient(135deg, #FFA500, #FF8C00);
}
.vip-subtitle {
font-size: 10px;
margin-top: 2px;
opacity: 0.9;
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 顶部导航条 */
.top-navbar {
height: 60px;
background-color: #ffffff;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.navbar-left .el-button-group {
margin-right: 20px;
}
.navbar-center {
flex: 1;
text-align: center;
}
.navbar-center .el-breadcrumb {
display: inline-block;
}
.navbar-right {
display: flex;
align-items: center;
gap: 10px;
}
/* 内容主体 */
.content-body {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f5f5f5;
height: auto;
min-height: auto;
}
/* 功能卡片 */
.feature-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
/* 欢迎区域 */
.welcome-section {
padding: 20px 0;
}
.welcome-card {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
text-align: center;
max-width: 600px;
margin: 0 auto;
}
.welcome-card h2 {
color: #333;
font-size: 28px;
margin-bottom: 16px;
font-weight: 600;
}
.welcome-card p {
color: #666;
font-size: 16px;
margin-bottom: 24px;
line-height: 1.6;
}
.welcome-card ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 20px;
}
.welcome-card li {
background: #f8f9fa;
padding: 12px 16px;
border-radius: 8px;
color: #555;
font-size: 14px;
border-left: 3px solid #409EFF;
}
.card-item {
background: white;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.card-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.card-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background: #f0f9ff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
}
.card-icon i {
font-size: 24px;
color: #409EFF;
}
.card-content h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.card-content p {
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* 数据统计区域 */
.stats-section {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 0;
margin-bottom: 15px;
padding: 0;
background: white;
border-radius: 0;
box-shadow: none;
border-bottom: 1px solid #e6e6e6;
width: 100%;
}
.stats-item {
text-align: left;
padding: 8px 12px;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 60px;
}
.stats-item:last-child {
border-right: none;
}
.stats-label {
font-size: 11px;
color: #333;
margin-bottom: 4px;
line-height: 1.2;
font-weight: 500;
}
.stats-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.stats-row:last-child {
margin-bottom: 0;
}
.stats-type {
font-size: 10px;
color: #666;
min-width: 30px;
}
.stats-value {
font-size: 12px;
font-weight: bold;
color: #333;
line-height: 1.1;
}
.stats-subtitle {
font-size: 10px;
color: #999;
margin-top: 2px;
line-height: 1.1;
}
.stats-unit {
font-size: 12px;
color: #999;
margin-top: 2px;
}
/* 筛选区域 */
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 12px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 表格区域 */
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
width: 100%;
}
.table-section .el-table {
width: 100% !important;
}
.table-section .el-table__body-wrapper {
width: 100% !important;
height:calc(100%)
}
/* 内容主体区域优化 */
.content-body {
flex: 1;
padding: 15px;
overflow-y: auto;
background-color: #f5f5f5;
width: 100%;
height: auto;
min-height: auto;
}
/* 确保所有容器都占满宽度 */
.filter-section,
.table-section,
.stats-section {
width: 100% !important;
box-sizing: border-box;
}
/* Element UI表格强制全宽 */
.el-table,
.el-table__header-wrapper,
.el-table__body-wrapper,
.el-table__footer-wrapper {
width: 100% !important;
}
.el-table .el-table__header,
.el-table .el-table__body {
width: 100% !important;
table-layout: fixed;
}
.product-info {
display: flex;
align-items: center;
}
.product-image-placeholder {
width: 50px;
height: 50px;
border-radius: 4px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 18px;
}
.product-details {
flex: 1;
}
.product-title {
font-size: 14px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
}
.product-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.budget-info, .roi-info, .period-info, .create-time {
font-size: 12px;
line-height: 1.4;
}
.budget-info .current-budget {
font-weight: bold;
color: #333;
}
.budget-info .target-budget {
color: #666;
}
.roi-info .roi-current {
color: #333;
}
.roi-info .roi-actual {
color: #666;
}
.ad-spend {
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.spend-link a {
color: #409EFF;
text-decoration: none;
font-size: 12px;
}
.spend-link a:hover {
text-decoration: underline;
}
.status-info {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.running {
background-color: #67C23A;
}
.status-dot.paused {
background-color: #E6A23C;
}
.status-dot.stopped {
background-color: #909399;
}
/* 分页 */
.pagination-section {
padding: 20px;
text-align: right;
border-top: 1px solid #f0f0f0;
}
/* 悬浮账号管理窗口 */
.account-float-window {
position: fixed;
top: 80px;
right: 20px;
width: 280px;
background: white;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
z-index: 1000;
overflow: hidden;
}
.float-window-header {
height: 40px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
border-bottom: 1px solid #e6e6e6;
}
.window-title {
font-size: 14px;
font-weight: 500;
color: #333;
}
.window-controls {
display: flex;
gap: 5px;
}
.float-window-content {
padding: 15px;
}
.account-list {
margin-bottom: 15px;
}
.account-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.account-item:last-child {
border-bottom: none;
}
.account-status {
margin-right: 10px;
}
.account-status .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.account-status .status-dot.online {
background-color: #67C23A;
}
.account-status .status-dot.offline {
background-color: #F56C6C;
}
.account-name {
margin-left: 10px;
font-size: 14px;
color: #333;
}
.account-actions {
display: flex;
gap: 10px;
}
.account-actions .el-button {
flex: 1;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.feature-cards {
grid-template-columns: repeat(2, 1fr);
}
.stats-section {
flex-wrap: wrap;
gap: 15px;
}
}
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.sidebar .menu-item-custom span {
display: none;
}
.feature-cards {
grid-template-columns: 1fr;
}
.filter-section {
flex-direction: column;
gap: 15px;
}
.account-float-window {
width: 250px;
right: 10px;
}
}
/* 自定义滚动条 */
.sidebar::-webkit-scrollbar,
.content-body::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track,
.content-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb,
.content-body::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.content-body::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 动画效果 */
.card-item,
.account-float-window,
.menu-item-custom {
transition: all 0.3s ease;
}
/* Element UI 组件样式覆盖 */
.el-table {
font-size: 12px;
}
.el-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.el-table td {
padding: 12px 0;
}
.el-tag--mini {
height: 20px;
line-height: 18px;
font-size: 10px;
}
.el-breadcrumb__inner {
color: #666;
font-weight: normal;
}
.el-breadcrumb__inner:hover {
color: #409EFF;
}
/* 状态标签颜色 */
.el-tag--success {
background-color: #f0f9ff;
border-color: #b3d8ff;
color: #409EFF;
}
.el-tag--warning {
background-color: #fdf6ec;
border-color: #f5dab1;
color: #e6a23c;
}
.el-tag--danger {
background-color: #fef0f0;
border-color: #fbc4c4;
color: #f56c6c;
}
.el-tag--info {
background-color: #f4f4f5;
border-color: #d3d4d6;
color: #909399;
}
/* 价格/费用标签与卖家样式 */
.price-tag {
color: #F56C6C;
font-weight: 600;
}
.fee-tag {
color: #E6A23C;
font-weight: 600;
}
.primary-seller {
color: #303133;
font-weight: 600;
}

View File

@@ -1,227 +0,0 @@
/* 更新对话框样式 */
.update-dialog .el-dialog {
border-radius: 16px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
}
.update-dialog .el-dialog__header {
display: block;
padding: 12px 20px 0 20px;
text-align: right;
}
.update-dialog .el-dialog__headerbtn {
top: 12px;
right: 20px;
width: 32px;
height: 32px;
font-size: 18px;
border-radius: 50%;
background-color: transparent;
}
.update-dialog .el-dialog__headerbtn:hover {
background-color: #f5f5f5;
}
.update-dialog .el-dialog__close {
color: #909399;
font-weight: 400;
}
.update-dialog .el-dialog__close:hover {
color: #409EFF;
}
.update-dialog .el-dialog__body {
padding: 0;
}
.update-content {
padding: 32px 24px 24px;
}
.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: 64px;
height: 64px;
border-radius: 12px;
margin-right: 16px;
flex-shrink: 0;
}
.update-header.text-center .app-icon {
margin-right: 0;
margin-bottom: 16px;
}
.update-header-content {
flex: 1;
min-width: 0;
}
.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 {
background-color: #f9fafb;
border-radius: 8px;
padding: 16px;
margin: 24px 0;
max-height: 200px;
overflow-y: auto;
}
.update-details h4 {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
}
.update-details p {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
margin: 0 0 16px 0;
}
.update-details p:last-child {
margin-bottom: 0;
}
.auto-update-section {
margin: 16px 0;
}
.auto-update-section .el-checkbox {
color: #374151;
font-size: 14px;
}
.update-actions {
margin-top: 24px;
}
.update-buttons {
display: flex;
justify-content: space-between;
gap: 12px;
}
.update-buttons.text-center {
justify-content: center;
}
.update-buttons .el-button {
flex: 1;
height: 40px;
font-size: 14px;
border-radius: 8px;
}
.update-buttons.text-center .el-button {
flex: none;
min-width: 140px;
}
/* 下载进度区域 */
.download-progress {
margin: 24px 0;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: #6b7280;
}
.download-note {
background-color: #fef3cd;
border: 1px solid #fcd34d;
border-radius: 8px;
padding: 12px;
margin: 16px 0;
}
.download-note p {
font-size: 12px;
color: #92400e;
margin: 0;
line-height: 1.5;
}
/* 进度条自定义样式 */
.el-progress-bar__outer {
border-radius: 4px;
background-color: #e5e7eb;
}
.el-progress-bar__inner {
border-radius: 4px;
transition: width 0.3s ease;
}
/* 按钮样式调整 */
.update-buttons .el-button--primary {
background-color: #2563eb;
border-color: #2563eb;
font-weight: 500;
}
.update-buttons .el-button--primary:hover {
background-color: #1d4ed8;
border-color: #1d4ed8;
}
.update-buttons .el-button:not(.el-button--primary) {
background-color: #f3f4f6;
border-color: #d1d5db;
color: #374151;
font-weight: 500;
}
.update-buttons .el-button:not(.el-button--primary):hover {
background-color: #e5e7eb;
border-color: #9ca3af;
}
/* 响应式调整 */
@media (max-width: 480px) {
.update-content {
padding: 24px 16px;
}
.update-buttons {
flex-direction: column;
}
.update-buttons .el-button {
flex: none;
}
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.web.WebView?>
<BorderPane fx:id="rootPane" prefHeight="600.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.tashow.erp.fx.controller.MainCtrl">
<center>
<WebView fx:id="webView" prefHeight="600.0" prefWidth="1000.0" BorderPane.alignment="CENTER" />
</center>
</BorderPane>

View File

@@ -1,714 +0,0 @@
<template>
<div>
<div class="main-container" :class="{ 'has-data': localProductData.length > 0, 'empty-data': localProductData.length === 0, 'loading-state': loading }">
<!-- 导入区域 -->
<div class="import-section">
<div class="import-controls">
<el-upload
class="excel-uploader"
action="#"
:http-request="handleExcelUpload"
:show-file-list="false"
accept=".xlsx,.xls"
:before-upload="beforeUpload"
:disabled="loading">
<el-button type="primary" :loading="loading" size="small">
<i class="el-icon-upload"></i> 导入ASIN列表
</el-button>
</el-upload>
<!-- 单个ASIN输入 -->
<div class="single-input">
<el-input
v-model="singleAsin"
placeholder="输入单个ASIN"
size="small"
style="width: 180px;"
:disabled="loading">
</el-input>
<el-button type="info" size="small" @click="searchSingleAsin" :disabled="!singleAsin || loading" style="margin-left: 8px;">
查询
</el-button>
</div>
<!-- 导出和停止按钮 -->
<div class="action-buttons">
<el-button type="danger" size="small" @click="stopFetch" :disabled="!loading">停止获取</el-button>
<el-button type="success" size="small" @click="exportToExcel" :disabled="!localProductData.length">导出Excel</el-button>
<el-button type="warning" size="small" @click="openGenmaiSpirit" :loading="genmaiLoading">跟卖精灵</el-button>
</div>
</div>
<div class="el-upload__tip">支持Excel批量导入(第一列为ASIN)或单个ASIN查询</div>
<!-- 进度条 -->
<div class="progress-section" v-if="loading">
<div class="progress-box">
<div class="progress-container">
<el-progress
:percentage="progressPercentage"
:status="progressPercentage >= 100 ? 'success' : null"
:stroke-width="6"
:show-text="false"
class="thin-progress">
</el-progress>
<div class="progress-text">{{progressPercentage}}%</div>
</div>
</div>
</div>
</div>
<!-- 数据表格容器 -->
<div class="table-container">
<!-- 无数据时的静态提示 -->
<div class="empty-loading-section" v-if="!loading && localProductData.length === 0">
<div class="empty-loading-container">
<i class="el-icon-document static-icon"></i>
<div class="empty-loading-text">暂无数据请导入ASIN列表</div>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section custom-scrollbar" v-if="localProductData.length > 0 || loading">
<el-table
:data="paginatedData"
style="width: 100%"
border
height="100%"
:cell-style="cellStyle"
:header-cell-style="headerCellStyle"
v-loading="tableLoading"
lazy>
<el-table-column prop="asin" label="ASIN" width="130" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'failed-data': (!scope.row.seller || scope.row.seller.trim() === '') || (!scope.row.price || scope.row.price.trim() === '') }">{{ scope.row.asin }}</span>
</template>
</el-table-column>
<el-table-column label="卖家/配送方" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'primary-seller': scope.row.seller, 'no-stock': !scope.row.seller }">{{ scope.row.seller || '无货' }}</span>
<span v-if="scope.row.shipper && scope.row.shipper !== scope.row.seller" class="shipper">配送方: {{ scope.row.shipper }}</span>
</template>
</el-table-column>
<el-table-column prop="price" label="当前售价" width="120">
<template slot-scope="scope">
<span :class="{ 'price-tag': scope.row.price, 'no-stock': !scope.row.price }">{{ scope.row.price || '无货' }}</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 固定分页组件 -->
<div class="pagination-fixed" v-if="localProductData.length > 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[15, 30, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="localProductData.length">
</el-pagination>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.shipper { margin-left:8px; color:#606266; }
.import-section { margin-bottom: 10px; }
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; }
.single-input { display: flex; align-items: center; }
.action-buttons { display: flex; gap: 10px; margin-top: 8px; }
.progress-section {
margin-bottom: 10px;
}
.progress-box {
padding: 8px 0;
margin-bottom: 0;
}
.progress-container {
display: flex;
align-items: center;
position: relative;
padding-right: 40px;
}
.progress-container .el-progress {
flex: 1;
}
.thin-progress .el-progress-bar__outer {
background-color: #ebeef5;
border-radius: 10px;
}
.thin-progress .el-progress-bar__inner {
border-radius: 10px;
}
.progress-text {
position: absolute;
right: 0;
font-size: 13px;
color: #409EFF;
font-weight: 500;
}
.main-container {
background-color: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* 表格容器布局 */
.table-container {
display: flex;
flex-direction: column;
height: calc(100vh - 220px); /* 根据需要调整高度 */
min-height: 400px;
}
.table-section {
flex: 1;
overflow: hidden;
/*padding-bottom: 80px;*/
/*margin-bottom: -80px!important;*/
display: flex;
flex-direction: column;
}
.table-section .el-table {
flex: 1;
height: 100%;
overflow-y: auto;
}
/* 固定分页样式 */
.pagination-fixed {
flex-shrink: 0;
padding: 10px 15px;
background-color: #f9f9f9;
border-radius: 4px;
display: flex;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: sticky;
bottom: 0;
z-index: 10;
margin-top: 0;
border-top: 1px solid #ebeef5;
height: 60px;
min-height: 60px;
}
/* 移除原来的分页样式 */
.pagination-section {
display: none;
}
/* 自定义滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 价格标签样式 */
.price-tag {
color: #e6a23c;
font-weight: bold;
}
/* 失败数据标记样式 */
.failed-data {
color: #f56c6c !important;
background-color: rgba(245, 108, 108, 0.1);
padding: 2px 4px;
border-radius: 3px;
font-weight: bold;
}
/* 无货数据样式 */
.no-stock {
color: #909399 !important;
font-style: italic;
background-color: rgba(144, 147, 153, 0.1);
padding: 2px 4px;
border-radius: 3px;
}
/* 表格滚动性能优化 */
.el-table {
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
/* 减少重绘 */
will-change: auto;
/* 强制使用复合层 */
backface-visibility: hidden;
}
.el-table__body-wrapper {
/* 禁用平滑滚动避免卡顿 */
scroll-behavior: auto;
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* 减少重绘和重排 */
.el-table .cell {
text-overflow: ellipsis;
white-space: nowrap;
}
/* 统一表格行高 */
.el-table td {
padding: 4px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
}
.el-table th {
padding: 6px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
}
.el-table .el-table__row {
height: 25px !important;
}
/* Loading图标旋转动画 */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 默认情况下所有loading图标都有动画 */
.el-icon-loading {
animation: rotate 2s linear infinite !important;
}
/* 表格loading遮罩动画 */
.el-loading-spinner .el-icon-loading {
animation: rotate 1.5s linear infinite !important;
}
/* 有数据时停止所有loading动画 */
.has-data .el-icon-loading,
.has-data .el-loading-spinner .el-icon-loading,
.has-data .el-button .el-icon-loading {
animation: none !important;
}
/* 导出按钮简单加载效果 */
.el-button.is-loading {
opacity: 0.8;
}
.el-button.is-loading .el-icon-loading {
animation: rotate 1s linear infinite !important;
}
/* 无数据时的加载状态样式 */
.empty-loading-section {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: white;
border-radius: 6px;
border: 1px solid #ebeef5;
}
.empty-loading-container {
text-align: center;
}
.empty-loading-container .static-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
display: block;
}
.empty-loading-text {
font-size: 14px;
color: #909399;
}
</style>
<script>
export default {
name: 'platform-amazon',
props: {
productData: { type: Array, default: function(){ return []; } }
},
data() {
return {
loading: false,
tableLoading: false,
progressPercentage: 0,
localProductData: [],
currentAsin: '',
importedAsinList: [],
singleAsin: '', // 单个ASIN输入
genmaiLoading: false,
// 分页相关数据
currentPage: 1,
pageSize: 15
}
},
mounted() {
// 页面加载时从localStorage恢复数据
this.loadDataFromStorage();
},
computed: {
// 分页后的数据
paginatedData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.localProductData.slice(start, end);
}
},
methods: {
cellStyle({ row, column, rowIndex, columnIndex }) {
return {
padding: '6px 8px',
'will-change': 'auto'
};
},
headerCellStyle({ row, column, rowIndex, columnIndex }) {
return {
padding: '8px',
'background-color': '#f5f7fa',
'font-weight': '500'
};
},
// 文件上传前验证
beforeUpload(file) {
try {
return fileService.validateFile(file);
} catch (error) {
this.$message.error(error.message);
return false;
}
},
async handleExcelUpload(options) {
try {
await this.clearBrowserCache();
// 强制停止任何正在进行的任务
this.loading = false;
this.tableLoading = false;
// 完全清空所有数据和状态
this.localProductData = [];
this.importedAsinList = [];
this.currentPage = 1;
this.progressPercentage = 0;
this.currentAsin = '';
try {
const wasInterrupted = localStorage.getItem('amazon-data-interrupted');
if (wasInterrupted) {
localStorage.removeItem('amazon-data-interrupted');
// 只在检测到中断时才清理数据
this.clearStorageData();
}
} catch (storageError) {
console.warn('localStorage access error:', storageError);
}
this.$emit('update:productData', []);
this.$emit('product-data-updated', []);
await this.$nextTick();
this.$forceUpdate();
this.loading = true;
this.tableLoading = true;
this.$forceUpdate();
const file = options.file;
try {
const asinList = await fileService.parseExcelForASIN(file);
this.importedAsinList = asinList;
this.$message.success(`成功导入 ${asinList.length} 个ASIN`);
await this.batchGetProductInfo(asinList);
} catch (error) {
console.error('解析Excel文件失败:', error);
this.$message.error(error.message || '解析Excel文件失败');
this.loading = false;
this.tableLoading = false;
}
} catch (error) {
console.error('上传文件失败:', error);
this.$message.error(this.getErrorMessage('上传文件失败: ' + error.message));
this.loading = false;
this.tableLoading = false;
}
},
async batchGetProductInfo(asinList) {
try {
this.currentAsin = '正在处理...';
this.progressPercentage = 0;
this.localProductData = []; // 完全清空现有数据
// 强制更新UI确保表格立即显示为空
this.$emit('update:productData', []);
this.$emit('product-data-updated', []);
await this.$nextTick();
this.$forceUpdate();
const batchId = `BATCH_${Date.now()}`;
const batchSize = 2; // 每批处理2个ASIN
const totalBatches = Math.ceil(asinList.length / batchSize);
let processedCount = 0;
let failedCount = 0;
// 改回2个一批的处理方式避免亚马逊风控检测
for (let i = 0; i < totalBatches && this.loading; i++) {
const start = i * batchSize;
const end = Math.min(start + batchSize, asinList.length);
const batchAsins = asinList.slice(start, end);
this.currentAsin = `正在处理第${i + 1}/${totalBatches}批 (${batchAsins.join(', ')})`;
try {
// 2个ASIN一批处理加入随机延迟避免风控
const result = await amazonAPI.getProductsBatch(batchAsins, batchId);
if (result && result.products && result.products.length > 0) {
this.localProductData.push(...result.products);
// 实时更新表格数据
this.$emit('update:productData', [...this.localProductData]);
this.$emit('product-data-updated', [...this.localProductData]);
if (this.tableLoading) {
this.tableLoading = false;
}
}
// 统计失败的ASIN数量
const expectedCount = batchAsins.length;
const actualCount = result?.products?.length || 0;
failedCount += Math.max(0, expectedCount - actualCount);
} catch (error) {
failedCount += batchAsins.length;
console.error(`批次${i + 1}失败:`, error.message);
}
processedCount += batchAsins.length;
this.progressPercentage = Math.round((processedCount / asinList.length) * 100);
// 增加随机延迟,避免请求过于频繁触发风控
if (i < totalBatches - 1 && this.loading) {
const delay = 1000 + Math.random() * 1500; // 1000-2500ms随机延迟
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// 保存数据到本地存储
if (this.localProductData.length > 0) {
this.saveDataToStorage();
}
// 处理完成
this.progressPercentage = 100;
this.currentAsin = '处理完成';
this.tableLoading = false;
} catch (error) {
this.$message.error(error.message || '批量获取产品信息失败');
this.currentAsin = '处理失败';
} finally {
this.loading = false;
this.tableLoading = false;
this.taskId = null;
}
},
// 清理浏览器缓存方法
async clearBrowserCache() {
try {
this.clearStorageData();
sessionStorage.clear();
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const db of databases) {
if (db.name) {
const deleteReq = window.indexedDB.deleteDatabase(db.name);
await new Promise(resolve => {
deleteReq.onsuccess = resolve;
deleteReq.onerror = resolve;
});
}
}
}
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
}
console.log('浏览器缓存已清理');
} catch (error) {
console.error('清理浏览器缓存失败:', error);
}
},
// 数据持久化方法
saveDataToStorage() {
// 数据已经在后端自动保存,这里不需要操作
// 保留方法避免其他地方调用报错
},
async loadDataFromStorage() {
try {
const response = await amazonAPI.getLatestProducts();
const productsData = response.products || [];
if (productsData.length > 0) {
this.localProductData = productsData;
this.importedAsinList = []; // 重置导入列表
this.$emit('update:productData', this.localProductData);
this.$emit('product-data-updated', this.localProductData);
}
} catch (error) {
console.error('加载最新数据失败:', error);
// 加载失败时不显示错误,保持页面正常
}
},
clearStorageData() {
// 数据已经在后端管理,这里不需要操作
// 保留方法避免其他地方调用报错
},
async exportToExcel() {
try {
if (!this.localProductData || this.localProductData.length === 0) {
this.$message.warning('没有数据可供导出');
return;
}
this.loading = true;
this.$message.info('正在生成Excel文件请稍候...');
// 准备导出数据
const exportData = this.localProductData.map(product => {
let sellerShipper = product.seller || '无货';
if (product.shipper && product.shipper !== product.seller) {
sellerShipper += (sellerShipper && sellerShipper !== '无货' ? ' / ' : '') + product.shipper;
}
return {
asin: product.asin || '',
seller_shipper: sellerShipper || '无货',
price: product.price || '无货'
};
});
const headers = [
{ header: 'ASIN', key: 'asin', width: 15 },
{ header: '卖家/配送方', key: 'seller_shipper', width: 35 },
{ header: '当前售价', key: 'price', width: 15 }
];
// 创建工作簿
const workbook = await fileService.createExcelWorkbook('Amazon产品数据', headers, exportData);
// 生成文件名
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xlsx`;
// 保存文件
const savedPath = await fileService.saveExcelToDesktop(workbook, fileName);
if (savedPath) {
this.$message.success(`Excel文件已保存到: ${savedPath}`);
} else {
this.$message.success('Excel文件导出成功');
}
} catch (error) {
console.error('导出Excel失败:', error);
this.$message.error(error.message || '导出Excel失败');
} finally {
this.loading = false;
}
},
stopFetch() {
console.log('停止获取产品数据');
this.loading = false;
this.currentAsin = '已停止';
// 标记数据已被中断,下次导入时需要完全清空
localStorage.setItem('amazon-data-interrupted', 'true');
this.$message.info('已停止获取产品数据');
},
// 分页相关方法
handleSizeChange(newSize) {
this.pageSize = newSize;
// 只有在改变页大小时才重置到第一页
this.currentPage = 1;
},
handleCurrentChange(newPage) {
this.currentPage = newPage;
},
// 单个ASIN查询
async searchSingleAsin() {
if (!this.singleAsin.trim()) return;
// 只清理当前组件数据不清理localStorage缓存
this.localProductData = [];
this.loading = true;
const asin = this.singleAsin.trim();
try {
const response = await amazonAPI.getProductsBatch([asin], `SINGLE_${Date.now()}`);
if (response.products?.length > 0) {
this.localProductData = response.products;
this.saveDataToStorage();
this.$message.success('查询成功');
this.singleAsin = '';
} else {
this.$message.warning('未找到商品信息');
}
} catch (error) {
this.$message.error(error.message || '查询失败');
}
this.loading = false;
},
async openGenmaiSpirit() {
this.genmaiLoading = true;
try {
await amazonAPI.openGenmaiSpirit();
} catch (error) {
this.$message.error(error.message || '打开跟卖精灵失败');
}
this.genmaiLoading = false;
},
},
}
</script>

View File

@@ -1,699 +0,0 @@
<template>
<div>
<div class="main-container">
<!-- 功能卡片区域 -->
<div class="feature-cards">
<div class="card-item"
v-for="card in featureCards"
:key="card.id"
:data-card-id="card.id"
@click="selectFeature(card.id)"
@mousedown="handleCardMouseDown(card.id)"
@mouseup="handleCardMouseUp(card.id)"
style="cursor: pointer; user-select: none;">
<div class="card-icon">
<i :class="card.icon"></i>
</div>
<div class="card-content">
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
</div>
</div>
</div>
<!-- 功能组件显示区域 -->
<div class="feature-content" v-if="selectedFeature">
<div v-if="selectedFeature === 1" id="ad-hosting-component"></div>
<div v-if="selectedFeature === 2" id="auto-review-component"></div>
<div v-if="selectedFeature === 3" id="flash-sale-auto-component"></div>
<div v-if="selectedFeature === 4" id="long-product-manage-component"></div>
</div>
<!-- 主组件内容区域 - 只在没有选择子功能时显示 -->
<div v-if="!selectedFeature">
<!-- 数据统计区域 -->
<div class="stats-section">
<div class="stats-item" v-for="stat in statsData" :key="stat.label">
<div class="stats-label">{{ stat.label }}</div>
<div class="stats-row" v-for="row in stat.rows" :key="row.type">
<span class="stats-type">{{ row.type }}</span>
<span class="stats-value">{{ row.value }}</span>
</div>
</div>
</div>
<!-- 筛选和状态切换 -->
<div class="filter-section">
<div class="filter-left">
<el-button-group>
<el-button
v-for="status in statusFilters"
:key="status.key"
:type="activeStatus === status.key ? 'primary' : ''"
size="small"
@click="setActiveStatus(status.key)">
{{ status.label }}
</el-button>
</el-button-group>
</div>
<div class="filter-right">
<el-input
placeholder="搜索"
v-model="searchText"
size="small"
style="width: 200px;">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</div>
</div>
</div>
</div>
</div>
</template>
<script scoped>
module.exports = {
name: 'platform-shopee',
props: {
// 可以接收从父组件传递的数据
onSearch: Function,
onImageError: Function
},
data() {
return {
// 选中的功能
selectedFeature: 1, // 默认选中广告投放托管
// 当前激活的状态筛选
activeStatus: '进行中',
// 搜索文本
searchText: '',
// 分页相关
currentPage: 1,
pageSize: 10,
total: 100,
// 功能卡片数据
featureCards: [
{
id: 1,
title: '广告投放托管',
description: '广告广告自动化托管',
icon: 'el-icon-s-promotion'
},
{
id: 2,
title: '自动回评',
description: '自动回复客户评价',
icon: 'el-icon-chat-dot-round'
},
{
id: 3,
title: '限时特卖自动化',
description: '限时特卖自动化设置上下架',
icon: 'el-icon-time'
},
{
id: 4,
title: '较长铺货商品',
description: '较长铺货商品管理',
icon: 'el-icon-goods'
}
],
// 统计数据
statsData: [
{
label: '广告营销周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: '新品/老品养成进度',
rows: [
{ type: '养定期', value: '7天' },
{ type: '老品', value: '7天' }
]
},
{
label: '预算/营销/营销时间',
rows: [
{ type: '每日', value: '119时30分' }
]
},
{
label: 'ROI变更周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: 'ROI自动优化',
rows: [
{ type: '新品', value: '3' },
{ type: '老品', value: '5' }
]
},
{
label: '新品/营销防控预算',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
}
],
// 状态筛选
statusFilters: [
{ key: '全部商品', label: '全部商品' },
{ key: '进行中', label: '进行中' },
{ key: '已排序', label: '已排序' },
{ key: '暂停中', label: '暂停中' },
{ key: '已结束', label: '已结束' },
{ key: '托管中', label: '托管中' },
{ key: '已归档', label: '已归档' }
],
// 表格数据
tableData: [
{
id: 1,
title: '商品标题商品标题商品标题商品标题商品标题商品标题',
tags: ['广告进行中', '新品/老品'],
budget: {
current: 'NT$ 100',
target: 'NT$ 180'
},
roi: {
target: '自由ROI',
current: '8'
},
adSpend: 'NT$ 27.86',
period: {
duration: '2',
updateTime: '07-09 17:00'
},
status: '进行中',
createTime: {
date: '2024-07-09',
time: '17:00',
creator: 'admin'
}
}
],
// 颜色数组
productColors: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
}
},
methods: {
// 选择功能
selectFeature(featureId) {
console.log('selectFeature 被调用featureId:', featureId);
try {
// 清理所有现有组件
this.clearAllComponents();
this.selectedFeature = featureId;
console.log('selectedFeature 设置为:', this.selectedFeature);
this.loadFeatureComponent(featureId);
} catch (error) {
console.error('selectFeature 执行失败:', error);
this.$message.error('切换功能失败,请重试');
}
},
// 返回主界面
backToMain() {
this.clearAllComponents();
this.selectedFeature = null;
},
// 清理所有组件
clearAllComponents() {
const componentMap = {
1: 'ad-hosting',
2: 'auto-review',
3: 'flash-sale-auto',
4: 'long-product-manage'
};
Object.values(componentMap).forEach(name => {
const container = document.getElementById(`${name}-component`);
if (container) {
// 销毁Vue实例
if (container.__vue__) {
container.__vue__.$destroy();
container.__vue__ = null;
}
container.innerHTML = '';
}
});
},
// 加载功能组件
loadFeatureComponent(featureId) {
const componentMap = {
1: 'ad-hosting',
2: 'auto-review',
3: 'flash-sale-auto',
4: 'long-product-manage'
};
const componentName = componentMap[featureId];
if (componentName) {
this.$nextTick(() => {
const container = document.getElementById(`${componentName}-component`);
if (container) {
// 使用axios代替fetch在JavaFX WebView中更稳定
axios.get(`/html/components/shopee/${componentName}.html`)
.then(response => response.data)
.then(html => {
// 清空其他组件
Object.values(componentMap).forEach(name => {
const otherContainer = document.getElementById(`${name}-component`);
if (otherContainer && name !== componentName) {
// 销毁Vue实例
if (otherContainer.__vue__) {
otherContainer.__vue__.$destroy();
}
otherContainer.innerHTML = '';
}
});
// 解析HTML内容并提取模板和脚本
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const template = tempDiv.querySelector('template');
const script = tempDiv.querySelector('script');
const style = tempDiv.querySelector('style');
// 添加样式
if (style && style.textContent) {
const styleElement = document.createElement('style');
styleElement.textContent = style.textContent;
document.head.appendChild(styleElement);
}
// 处理脚本和组件注册
if (script && script.textContent) {
let scriptContent = script.textContent.trim();
if (scriptContent.includes('module.exports')) {
// 提取组件名称
const nameMatch = scriptContent.match(/name:\s*['"`]([^'"`]+)['"`]/);
const extractedComponentName = nameMatch ? nameMatch[1] : componentName;
// 替换 module.exports 为直接的组件对象
scriptContent = scriptContent.replace(/module\.exports\s*=\s*/, '');
try {
// 创建组件配置对象
const componentConfig = eval('(' + scriptContent + ')');
// 注册为 Vue 组件
if (componentConfig && extractedComponentName) {
// 从 template 标签提取模板内容
if (template && template.innerHTML) {
componentConfig.template = template.innerHTML;
}
// 检查组件是否已注册,避免重复注册
const componentKey = `${extractedComponentName}-${featureId}`;
if (!Vue.options.components[componentKey]) {
Vue.component(componentKey, componentConfig);
console.log(`子组件 ${componentKey} 已成功注册,模板长度:`, componentConfig.template ? componentConfig.template.length : 0);
}
// 销毁现有的Vue实例如果有
if (container.__vue__) {
container.__vue__.$destroy();
}
// 使用Vue实例渲染组件
container.innerHTML = `<${componentKey}></${componentKey}>`;
// 延迟创建Vue实例确保DOM更新完成
this.$nextTick(() => {
try {
// 创建新的Vue实例
const vueInstance = new Vue({
el: container,
data() {
return {};
},
mounted() {
console.log(`子组件Vue实例已挂载: ${componentKey}`);
},
errorCaptured(err, instance, info) {
console.error(`子组件Vue错误: ${err.message}`, err);
return false;
}
});
console.log(`Vue实例创建成功: ${componentKey}`);
} catch (error) {
console.error(`创建Vue实例失败: ${componentKey}`, error);
// 降级处理:直接插入模板内容
if (template) {
container.innerHTML = template.innerHTML;
}
}
});
}
} catch (error) {
console.error('解析子组件脚本失败:', error);
console.error('脚本内容:', scriptContent);
// 降级处理:直接插入模板内容
if (template) {
container.innerHTML = template.innerHTML;
}
}
} else {
// 普通脚本执行
const newScript = document.createElement('script');
newScript.textContent = scriptContent;
document.head.appendChild(newScript);
document.head.removeChild(newScript);
// 插入模板内容
if (template) {
container.innerHTML = template.innerHTML;
}
}
} else if (template) {
// 只有模板没有脚本的情况
container.innerHTML = template.innerHTML;
}
})
.catch(error => {
console.error('加载组件失败:', error);
this.$message.error('组件加载失败');
});
}
});
}
},
// 设置激活状态
setActiveStatus(status) {
this.activeStatus = status;
},
// 获取标签类型
getTagType(tag) {
if (tag.includes('进行中')) return 'success';
if (tag.includes('新品')) return 'warning';
if (tag.includes('老品')) return 'info';
return '';
},
// 获取状态样式类
getStatusClass(status) {
switch(status) {
case '进行中': return 'running';
case '暂停中': return 'paused';
case '已归档': return 'stopped';
default: return 'stopped';
}
},
// 获取商品图片背景颜色
getProductColor(id) {
return this.productColors[(id - 1) % this.productColors.length];
},
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
},
// JavaFX WebView兼容性检查
checkJavaFXCompatibility() {
try {
// 检测是否在JavaFX WebView环境中
const isJavaFX = navigator.userAgent.includes('Java') ||
window.javaConnector !== undefined ||
window.java !== undefined;
if (isJavaFX) {
console.log('检测到JavaFX WebView环境启用兼容模式');
// 为所有功能卡片添加原生点击事件处理
this.$nextTick(() => {
this.addNativeClickHandlers();
});
} else {
console.log('标准浏览器环境');
}
} catch (error) {
console.warn('环境检测失败:', error);
}
},
// 添加原生点击事件处理
addNativeClickHandlers() {
try {
this.featureCards.forEach(card => {
// 为每个功能卡片添加原生事件监听
const cardElement = document.querySelector(`[data-card-id="${card.id}"]`);
if (cardElement) {
cardElement.addEventListener('click', (event) => {
console.log('原生点击事件触发cardId:', card.id);
this.selectFeature(card.id);
});
}
});
console.log('原生点击事件处理器已添加');
} catch (error) {
console.error('添加原生点击事件失败:', error);
}
},
// 调试方法:鼠标按下
handleCardMouseDown(cardId) {
console.log('鼠标按下事件cardId:', cardId);
},
// 调试方法:鼠标松开
handleCardMouseUp(cardId) {
console.log('鼠标松开事件cardId:', cardId);
}
},
mounted() {
console.log('虾皮平台组件已加载');
// JavaFX WebView兼容性检查
this.checkJavaFXCompatibility();
// 初始加载广告投放托管组件
this.loadFeatureComponent(1);
},
beforeDestroy() {
// 组件销毁前清理所有子组件
this.clearAllComponents();
}
}
</script>
<style scoped>
.feature-content {
height: auto;
overflow: visible;
}
.main-container {
padding: 20px;
height: auto;
overflow: visible;
}
.feature-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.card-item {
background: white;
border-radius: 8px;
padding: 20px;
border: 1px solid #e4e7ed;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
}
.card-item:hover {
border-color: #409EFF;
box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.15);
transform: translateY(-2px);
}
.card-icon {
margin-right: 16px;
}
.card-icon i {
font-size: 32px;
color: #409EFF;
}
.card-content h3 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
.card-content p {
margin: 0;
color: #606266;
font-size: 14px;
}
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.stats-item {
background: white;
padding: 16px;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.stats-label {
font-weight: 600;
color: #303133;
margin-bottom: 8px;
font-size: 14px;
}
.stats-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
}
.stats-type {
color: #606266;
}
.stats-value {
color: #303133;
font-weight: 500;
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.table-section {
background: white;
border-radius: 6px;
overflow: hidden;
}
.product-info {
display: flex;
align-items: center;
}
.product-image-placeholder {
width: 40px;
height: 40px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.product-details {
flex: 1;
}
.product-title {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-tags {
display: flex;
gap: 4px;
}
.budget-info, .roi-info, .period-info, .create-time {
font-size: 12px;
}
.current-budget, .roi-current {
color: #303133;
margin-bottom: 2px;
}
.target-budget, .roi-actual {
color: #909399;
}
.ad-spend {
font-weight: 600;
color: #E6A23C;
margin-bottom: 4px;
}
.spend-link a {
color: #409EFF;
text-decoration: none;
font-size: 12px;
}
.status-info {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.running {
background-color: #67C23A;
}
.status-dot.paused {
background-color: #E6A23C;
}
.status-dot.stopped {
background-color: #F56C6C;
}
.pagination-section {
padding: 20px;
text-align: right;
}
</style>

View File

@@ -1,935 +0,0 @@
<template>
<div>
<div class="main-container">
<!-- 日期选择区域 -->
<div class="date-filter-section">
<el-select v-model="selectedShops" multiple placeholder="选择店铺" style="width: 300px;">
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id"></el-option>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions">
</el-date-picker>
<el-button type="primary" size="small" @click="fetchData" :disabled="loading" :loading="loading">
<i class="el-icon-loading" v-if="loading"></i>
<i class="el-icon-search" v-if="!loading"></i>
{{ loading ? '获取中...' : '获取订单数据' }}
</el-button>
<el-button type="danger" size="small" @click="stopFetch" :disabled="!loading">停止获取</el-button>
<el-button type="success" size="small" @click="exportToExcel" :disabled="loading || !allOrderData.length || exportLoading" :loading="exportLoading">
<i class="el-icon-download" v-if="!exportLoading"></i>
<i class="el-icon-loading" v-if="exportLoading"></i>
{{ exportLoading ? '导出中...' : '导出Excel' }}
</el-button>
<el-button type="warning" size="small" @click="refreshToken" :loading="tokenRefreshing">
<i class="el-icon-refresh" v-if="!tokenRefreshing"></i>
刷新认证
</el-button>
</div>
<!-- 进度条区域 -->
<div class="progress-section">
<div class="progress-box">
<div class="progress-container">
<el-progress
:percentage="progressPercentage"
:status="progressPercentage >= 100 ? 'success' : (progressStatus === 'exception' ? 'exception' : null)"
:stroke-width="6"
:show-text="false"
class="thin-progress">
</el-progress>
<div class="progress-text">{{progressPercentage}}%</div>
</div>
</div>
</div>
<!-- 表格容器 -->
<div class="table-container">
<div class="table-section custom-scrollbar" v-show="paginatedData && paginatedData.length >= 0">
<el-table :data="paginatedData"
style="width: 100%"
border
stripe
lazy
height="100%"
:cell-style="cellStyle"
:header-cell-style="headerCellStyle"
v-loading="tableLoading"
element-loading-text="正在获取订单数据..."
element-loading-spinner="el-icon-loading"
>
<el-table-column prop="orderedAt" label="下单时间" width="120" show-overflow-tooltip></el-table-column>
<el-table-column label="商品图片" width="80">
<template slot-scope="scope">
<div class="image-container" v-if="scope.row.productImage">
<el-image
:src="scope.row.productImage"
@error="onImageError(scope.row)"
class="thumb"
:preview-src-list="[scope.row.productImage]"
fit="contain"
loading="lazy"
preview-teleported
:z-index="3000">
<div slot="placeholder" class="image-placeholder">
<i class="el-icon-loading"></i>
</div>
<div slot="error" class="image-placeholder">
<i class="el-icon-picture"></i>
</div>
</el-image>
</div>
<span v-else>无图片</span>
</template>
</el-table-column>
<el-table-column prop="productTitle" label="商品名称" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="shopOrderNumber" label="乐天订单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="timeSinceOrder" label="下单距今" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="priceJpy" label="订单金额/日元" width="120">
<template slot-scope="scope">
<span class="price-tag">{{ formatJpy(scope.row.priceJpy) }}</span>
</template>
</el-table-column>
<el-table-column prop="productQuantity" label="数量" width="60"></el-table-column>
<el-table-column prop="shippingFeeJpy" label="税费/日元" width="100">
<template slot-scope="scope">
<span class="fee-tag">{{ formatJpy(scope.row.shippingFeeJpy) }}</span>
</template>
</el-table-column>
<el-table-column label="回款抽点rmb" width="120" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.serviceFee || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="productNumber" label="商品番号" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="poNumber" label="1688订单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="shippingFeeCny" label="采购金额/rmb" width="120">
<template slot-scope="scope">
<span class="fee-tag">{{ formatCny(scope.row.shippingFeeCny) }}</span>
</template>
</el-table-column>
<el-table-column prop="internationalShippingFee" label="国际运费/rmb" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="poLogisticsCompany" label="国内物流" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="poTrackingNumber" label="国内单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="internationalTrackingNumber" label="日本单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="trackInfo" label="地址状态" width="120" show-overflow-tooltip>
<template slot-scope="scope">
<template v-if="scope.row.trackInfo">
<el-tooltip :content="scope.row.trackInfo" placement="top" effect="dark">
<el-tag size="mini">{{ scope.row.trackInfo }}</el-tag>
</el-tooltip>
</template>
<span v-else>暂无</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 固定分页组件 -->
<div class="pagination-fixed" v-if="allOrderData.length > 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[15, 30, 50, 100, 200]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="allOrderData.length">
</el-pagination>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.thumb {
width: 40px;
height: 40px;
object-fit: contain; /* 保持图片宽高比 */
border-radius: 4px;
cursor: pointer;
transition: none;
backface-visibility: hidden;
transform: translateZ(0);
}
.thumb:hover {
transform: scale(1.05);
transition: transform 0.2s ease;
}
/* 图片容器样式 */
.image-container {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 20px;
margin: 0 auto;
background-color: #f8f9fa;
border-radius: 2px;
transform: translateZ(0);
}
.image-container .el-image {
width: 16px;
height: 16px;
border-radius: 2px;
}
/* 图片占位符样式 */
.image-placeholder {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
color: #c0c4cc;
font-size: 10px;
border-radius: 2px;
}
.main-container {
background-color: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.progress-section {
margin-bottom: 10px;
}
.progress-box {
padding: 8px 0;
margin-bottom: 0;
}
.progress-container {
display: flex;
align-items: center;
position: relative;
padding-right: 40px;
}
.progress-container .el-progress {
flex: 1;
}
.thin-progress .el-progress-bar__outer {
background-color: #ebeef5;
border-radius: 10px;
}
.thin-progress .el-progress-bar__inner {
border-radius: 10px;
}
.progress-text {
position: absolute;
right: 0;
font-size: 13px;
color: #409EFF;
font-weight: 500;
}
/* Loading图标旋转动画 */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.el-icon-loading {
animation: rotate 2s linear infinite !important;
}
/* 表格loading遮罩动画 */
.el-loading-spinner .el-icon-loading {
animation: rotate 1.5s linear infinite !important;
}
/* 表格内loading图标 */
.el-table .el-icon-loading {
animation: rotate 1s linear infinite !important;
display: inline-block;
}
/* 导出按钮简单加载效果 */
.el-button.is-loading {
opacity: 0.8;
}
.el-button.is-loading .el-icon-loading {
animation: rotate 1s linear infinite !important;
}
/* 进度条加载状态动画 */
.progress-section {
animation: slideIn 0.3s ease-in-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 脉冲动画 */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
/* 加载状态的脉冲效果 */
.el-progress {
animation: pulse 2s ease-in-out infinite;
}
/* 进度条内部条纹动画 */
.el-progress-bar__inner {
background-image: linear-gradient(45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent) !important;
background-size: 20px 20px !important;
animation: progressStripes 1s linear infinite, pulse 2s ease-in-out infinite !important;
}
@keyframes progressStripes {
0% {
background-position: 0 0;
}
100% {
background-position: 20px 0;
}
}
/* 表格加载时的渐入效果 */
.el-table tbody tr {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 按钮悬停效果增强 */
.el-button {
transition: all 0.3s ease;
transform: translateZ(0);
}
.el-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.el-button:active:not(:disabled) {
transform: translateY(0);
}
/* 表格行悬停效果增强 */
.el-table tbody tr {
transition: all 0.3s ease;
}
.el-table tbody tr:hover {
transform: translateZ(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 进度文本闪烁效果 */
.progress-text {
animation: textGlow 2s ease-in-out infinite;
}
@keyframes textGlow {
0%, 100% {
text-shadow: 0 0 5px rgba(64, 158, 255, 0.3);
}
50% {
text-shadow: 0 0 15px rgba(64, 158, 255, 0.8);
}
}
.progress-actions {
display: flex;
justify-content: flex-end;
}
.date-filter-section {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 15px;
}
.table-section {
overflow-y: auto;
margin-bottom: 10px;
}
.table-actions {
margin-bottom: 15px;
display: flex;
justify-content: flex-end;
}
/* 表格容器布局 */
.table-container {
display: flex;
flex-direction: column;
height: calc(100vh - 230px);
min-height: 400px;
}
.table-section {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
margin-bottom: 0;
}
.table-section .el-table {
flex: 1;
height: 100%;
overflow-y: auto;
}
/* 固定分页样式 */
.pagination-fixed {
flex-shrink: 0;
padding: 10px 15px;
background-color: #f9f9f9;
border-radius: 4px;
display: flex;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: sticky;
bottom: 0;
z-index: 10;
border-top: 1px solid #ebeef5;
height: 60px;
min-height: 60px;
}
/* 自定义滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 图片预览优化 */
.el-image-viewer__wrapper {
z-index: 3000 !important;
}
.el-image-viewer__mask {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 表格内滚动条样式 */
.el-table__body-wrapper::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.el-table__body-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.el-table__body-wrapper::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
.el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 价格和费用标签样式 */
.price-tag {
color: #e6a23c;
font-weight: bold;
}
.fee-tag {
color: #909399;
font-weight: 500;
}
/* 操作按钮样式优化 */
.el-table .el-button--mini {
padding: 5px 8px;
font-size: 12px;
}
/* 表格行悬停效果 */
.el-table tbody tr:hover {
background-color: #f5f7fa;
}
/* 表格滚动性能优化 */
.el-table {
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
/* 减少重绘 */
will-change: auto;
/* 强制使用复合层 */
backface-visibility: hidden;
}
.el-table__body-wrapper {
/* 禁用平滑滚动避免卡顿 */
scroll-behavior: auto;
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* 减少重绘和重排 */
.el-table .cell {
text-overflow: ellipsis;
white-space: nowrap;
}
/* 优化表格行高度 */
.el-table td {
padding: 4px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
}
.el-table th {
padding: 6px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
background-color: #fafafa;
color: #606266;
font-weight: 600;
}
.el-table .el-table__row {
height: 25px !important;
}
/* 表格滚动性能优化 */
.el-table {
transform: translateZ(0);
-webkit-transform: translateZ(0);
backface-visibility: hidden;
will-change: auto;
}
.el-table__body-wrapper {
scroll-behavior: auto;
transform: translateZ(0);
-webkit-transform: translateZ(0);
-webkit-overflow-scrolling: touch;
transition: none;
}
</style>
<script>
export default {
name: 'platform-zebra',
props: {
orderData: { type: Array, default: function(){ return []; } },
formatJpy: { type: Function, default: function(v){ return '¥' + (Number(v) || 0); } },
formatCny: { type: Function, default: function(v){ return '¥' + (Number(v) || 0); } },
onImageError: { type: Function, default: function(){} }
},
watch: {
// 监听外部传入的orderData变化
orderData: {
handler(newData, oldData) {
if (newData && newData.length > 0) {
// 保存当前页码
const currentPageBackup = this.currentPage;
// 判断是否是全新数据(初次加载)还是增量更新
const isInitialLoad = !oldData || oldData.length === 0;
const isCompleteReset = oldData && oldData.length > 0 && newData.length < oldData.length;
this.allOrderData = [...newData];
this.totalItems = this.allOrderData.length;
// 只有在初次加载或完全重置时才跳转到第一页
if (isInitialLoad || isCompleteReset) {
this.currentPage = 1;
} else {
// 增量更新时保持当前页码,但需要检查页码是否超出范围
const maxPage = Math.ceil(newData.length / this.pageSize);
this.currentPage = Math.min(currentPageBackup, Math.max(1, maxPage));
}
}
},
immediate: true
}
},
data() {
return {
loading: false,
tableLoading: false,
progressPercentage: 0,
progressMessage: '',
progressStatus: '',
dateRange: [],
selectedShops: [],
shopList: [],
// 分页相关数据
currentPage: 1,
pageSize: 15,
totalItems: 0,
allOrderData: [], // 存储所有订单数据
// 爬取相关数据
fetchCurrentPage: 1,
fetchTotalPages: 0,
fetchTotalItems: 0,
isFetching: false,
exportLoading: false,
tokenRefreshing: false,
pickerOptions: {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
mounted() {
// 页面加载时获取店铺列表和从localStorage恢复数据
this.loadShops();
this.loadDataFromStorage();
},
computed: {
// 分页后的数据
paginatedData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.allOrderData.slice(start, end);
}
},
methods: {
// 获取店铺列表
async loadShops() {
try {
const response = await zebraAPI.getShops();
if (response && response.data && response.data.list) {
this.shopList = response.data.list;
}
} catch (error) {
console.error('获取店铺列表失败:', error);
}
},
fetchData() {
Object.assign(this, {
loading: true,
tableLoading: true,
progressPercentage: 0,
progressStatus: '',
progressMessage: '',
allOrderData: [],
fetchCurrentPage: 1,
fetchTotalPages: 0,
fetchTotalItems: 0,
isFetching: true,
totalItems: 0,
currentPage: 1,
currentBatchId: `ZEBRA_${Date.now()}`
});
// 获取日期范围
const [startDate = '', endDate = ''] = this.dateRange || [];
this.fetchPageData(startDate, endDate);
},
fetchPageData(startDate, endDate) {
if (!this.isFetching) {
return;
}
// 使用zebraAPI获取当前页数据
zebraAPI.getOrders({
startDate,
endDate,
page: this.fetchCurrentPage,
pageSize: 10, // 每页10条数据
batchId: this.currentBatchId,
shopIds: this.selectedShops && this.selectedShops.length > 0 ? this.selectedShops.join(',') : ''
})
.then(data => {
// 获取成功,处理数据
const orders = data.orders || [];
this.allOrderData = [...this.allOrderData, ...orders];
// 有数据后立即停止表格loading
if (this.allOrderData.length > 0) {
this.tableLoading = false;
}
// 触发响应式更新(移除$forceUpdate依赖Vue响应式机制
this.$nextTick();
// 保存数据到localStorage
this.saveDataToStorage();
// 通知父组件数据更新
this.$emit('order-data-updated', this.allOrderData);
// 更新总数据信息
this.fetchTotalPages = data.totalPages || 0;
this.fetchTotalItems = data.total || 0;
// 使用当前实际获取的数据条数,而不是后端返回的总数
this.totalItems = this.allOrderData.length;
// 更新进度 - 改为显示已获取的数据条数占总数的百分比
const currentCount = this.allOrderData.length;
this.progressPercentage = Math.min(100, Math.round((currentCount / this.fetchTotalItems) * 100));
// 判断是否继续获取下一页
if (this.fetchCurrentPage < this.fetchTotalPages && this.isFetching) {
this.fetchCurrentPage++;
// 延迟一点时间再请求下一页,避免请求过于频繁
setTimeout(() => {
this.fetchPageData(startDate, endDate);
}, 300);
} else {
// 全部获取完成
this.finishFetching();
}
})
.catch(error => {
console.error('获取订单数据失败:', error);
this.$message.error(error.message || '获取订单数据失败');
this.finishFetching(false);
});
},
finishFetching(success = true) {
this.isFetching = false;
this.loading = false;
this.tableLoading = false;
if (success) {
this.progressStatus = '';
this.progressPercentage = 100;
this.totalItems = this.allOrderData.length;
// 通知父组件更新数据
this.$emit('update:orderData', this.allOrderData);
this.$emit('order-data-updated', this.allOrderData);
} else {
this.progressStatus = 'exception';
}
},
stopFetch() {
console.log('停止获取订单数据');
this.isFetching = false;
this.loading = false; // 重置loading状态使获取订单按钮重新可用
this.tableLoading = false; // 停止表格loading
this.$message.info('已停止获取订单数据');
},
// 分页相关方法
handleSizeChange(newSize) {
this.pageSize = newSize;
// 只有在改变页大小时才重置到第一页
this.currentPage = 1;
},
handleCurrentChange(newPage) {
this.currentPage = newPage;
},
// 性能优化:简化数据存储方法,移除空方法
saveDataToStorage() {}, // 空实现,后端自动保存
async loadDataFromStorage() {
const response = await zebraAPI.getLatestOrders().catch(error => {
console.error('加载最新数据失败:', error);
return { orders: [] };
});
const ordersData = response.orders || [];
if (ordersData.length > 0) {
Object.assign(this, {
allOrderData: ordersData,
dateRange: [],
totalItems: ordersData.length,
tableLoading: false
});
this.$emit('order-data-updated', this.allOrderData);
}
},
// 性能优化简化刷新token逻辑
async refreshToken() {
this.tokenRefreshing = true;
const result = await zebraAPI.refreshToken().catch(error => {
console.error('刷新认证失败:', error);
this.$message.error(error.message || '认证刷新失败');
return null;
});
if (result) this.$message.success('认证刷新成功');
this.tokenRefreshing = false;
},
// 导出Excel方法 - 多线程优化版本
async exportToExcel() {
try {
if (!this.allOrderData || this.allOrderData.length === 0) {
this.$message.warning('没有数据可供导出');
return;
}
// JavaFX环境检查可选
// if (!window.javaConnector) {
// this.$message.error('此功能仅在JavaFX环境中可用');
// return;
// }
// 防止重复点击
if (this.exportLoading) {
return;
}
this.exportLoading = true;
// 显示导出提示
this.$message({
message: '正在生成Excel文件请稍候...',
type: 'info',
duration: 3000
});
// 生成文件名和导出数据
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const fileName = `斑马订单数据_${timestamp}.xlsx`;
const exportData = {
orders: this.allOrderData,
title: '斑马订单数据导出',
fileName: fileName,
timestamp: new Date().toLocaleString('zh-CN'),
useMultiThread: true, // 启用多线程处理
chunkSize: 1000 // 每个线程处理1000条数据
};
const result = await zebraAPI.exportAndSaveOrders(exportData);
this.$message.closeAll();
this.$message.success(`Excel文件已保存到: ${result.filePath}`);
} catch (error) {
console.error('导出Excel失败:', error);
this.$message.closeAll();
this.$message.error(error.message || '导出Excel失败');
} finally {
this.exportLoading = false;
}
},
progressFormat(percentage) {
if (percentage === 100) return '完成';
if (this.allOrderData.length > 0) {
return `${this.allOrderData.length}/${this.fetchTotalItems}`;
}
return `${percentage}%`;
},
// 性能优化:简化批次数据加载
async loadBatchData(batchId) {
const response = await zebraAPI.getOrdersByBatch(batchId).catch(error => {
console.error('获取批次数据失败:', error);
return { orders: [] };
});
const orders = response.orders || [];
if (orders.length > 0) {
this.allOrderData = orders;
this.totalItems = orders.length;
this.$emit('order-data-updated', this.allOrderData);
}
},
// 表格单元格样式
cellStyle() {
return {
'font-size': '12px',
'padding': '8px 4px'
};
},
// 表格表头样式
headerCellStyle() {
return {
'background-color': '#f5f7fa',
'color': '#303133',
'font-weight': 'bold',
'font-size': '13px',
'padding': '8px 4px'
};
}
},
beforeDestroy() {
// 组件销毁前停止获取
this.isFetching = false;
}
}
</script>

View File

@@ -1,431 +0,0 @@
<template>
<div class="ad-hosting-container">
<!-- 数据统计区域 -->
<div class="stats-section">
<div class="stats-item" v-for="stat in statsData" :key="stat.label">
<div class="stats-label">{{ stat.label }}</div>
<div class="stats-row" v-for="row in stat.rows" :key="row.type">
<span class="stats-type">{{ row.type }}</span>
<span class="stats-value">{{ row.value }}</span>
</div>
</div>
</div>
<!-- 筛选和状态切换 -->
<div class="filter-section">
<div class="filter-left">
<el-button-group>
<el-button
v-for="status in statusFilters"
:key="status.key"
:type="activeStatus === status.key ? 'primary' : ''"
size="small"
@click="setActiveStatus(status.key)">
{{ status.label }}
</el-button>
</el-button-group>
</div>
<div class="filter-right">
<el-input
placeholder="搜索商品"
v-model="searchText"
size="small"
style="width: 200px;">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<el-table :data="tableData" style="width: 100%">
<el-table-column label="商品信息" width="300">
<template slot-scope="scope">
<div class="product-info">
<div class="product-image-placeholder" :style="{ backgroundColor: getProductColor(scope.row.id) }">
{{ scope.row.title.substr(0, 1) }}
</div>
<div class="product-details">
<div class="product-title">{{ scope.row.title }}</div>
<div class="product-tags">
<el-tag v-for="tag in scope.row.tags" :key="tag" size="mini" :type="getTagType(tag)">
{{ tag }}
</el-tag>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="budget" label="每日预算" width="120">
<template slot-scope="scope">
<div class="budget-info">
<div class="current-budget">{{ scope.row.budget.current }}</div>
<div class="target-budget">{{ scope.row.budget.target }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="roi" label="目标投入产出比" width="150">
<template slot-scope="scope">
<div class="roi-info">
<div class="roi-current">ROI目标{{ scope.row.roi.target }}</div>
<div class="roi-actual">当前ROI{{ scope.row.roi.current }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="adSpend" label="广告花费" width="120">
<template slot-scope="scope">
<div class="ad-spend">{{ scope.row.adSpend }}</div>
<div class="spend-link">
<a href="#" @click.prevent>历史记录</a>
</div>
</template>
</el-table-column>
<el-table-column prop="period" label="投放周期" width="120">
<template slot-scope="scope">
<div class="period-info">
<div>投放周期:{{ scope.row.period.duration }}</div>
<div>更新时间:{{ scope.row.period.updateTime }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="托管状态" width="120">
<template slot-scope="scope">
<div class="status-info">
<span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
<span>{{ scope.row.status }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="150">
<template slot-scope="scope">
<div class="create-time">
<div>创建时间:{{ scope.row.createTime.date }}</div>
<div>创建时间:{{ scope.row.createTime.time }}</div>
<div>创建时间:{{ scope.row.createTime.creator }}</div>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
module.exports = {
name: 'ad-hosting',
data() {
return {
// 当前激活的状态筛选
activeStatus: '进行中',
// 搜索文本
searchText: '',
// 分页相关
currentPage: 1,
pageSize: 10,
total: 100,
// 统计数据
statsData: [
{
label: '广告营销周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: '新品/老品养成进度',
rows: [
{ type: '养定期', value: '7天' },
{ type: '老品', value: '7天' }
]
},
{
label: '预算/营销/营销时间',
rows: [
{ type: '每日', value: '119时30分' }
]
},
{
label: 'ROI变更周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: 'ROI自动优化',
rows: [
{ type: '新品', value: '3' },
{ type: '老品', value: '5' }
]
},
{
label: '新品/营销防控预算',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
}
],
// 状态筛选
statusFilters: [
{ key: '全部商品', label: '全部商品' },
{ key: '进行中', label: '进行中' },
{ key: '已排序', label: '已排序' },
{ key: '暂停中', label: '暂停中' },
{ key: '已结束', label: '已结束' },
{ key: '托管中', label: '托管中' },
{ key: '已归档', label: '已归档' }
],
// 表格数据
tableData: [
{
id: 1,
title: '广告托管商品1 - 自动化投放优化',
tags: ['广告进行中', '新品'],
budget: {
current: 'NT$ 150',
target: 'NT$ 200'
},
roi: {
target: '自动ROI',
current: '6.5'
},
adSpend: 'NT$ 45.50',
period: {
duration: '5',
updateTime: '07-16 10:30'
},
status: '进行中',
createTime: {
date: '2024-07-16',
time: '10:30',
creator: 'admin'
}
},
{
id: 2,
title: '广告托管商品2 - 智能出价管理',
tags: ['广告进行中', '老品'],
budget: {
current: 'NT$ 200',
target: 'NT$ 300'
},
roi: {
target: '目标ROI 8',
current: '7.2'
},
adSpend: 'NT$ 78.20',
period: {
duration: '10',
updateTime: '07-16 14:20'
},
status: '托管中',
createTime: {
date: '2024-07-10',
time: '14:20',
creator: 'admin'
}
}
],
// 颜色数组
productColors: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
}
},
methods: {
// 设置激活状态
setActiveStatus(status) {
this.activeStatus = status;
},
// 获取标签类型
getTagType(tag) {
if (tag.includes('进行中')) return 'success';
if (tag.includes('新品')) return 'warning';
if (tag.includes('老品')) return 'info';
return '';
},
// 获取状态样式类
getStatusClass(status) {
switch(status) {
case '进行中': return 'running';
case '托管中': return 'hosting';
case '暂停中': return 'paused';
case '已归档': return 'stopped';
default: return 'stopped';
}
},
// 获取商品图片背景颜色
getProductColor(id) {
return this.productColors[(id - 1) % this.productColors.length];
},
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
}
},
mounted() {
console.log('广告投放托管组件已加载');
}
}
</script>
<style scoped>
.ad-hosting-container {
padding: 20px;
}
.header-section {
margin-bottom: 20px;
}
.header-section h2 {
color: #303133;
margin-bottom: 8px;
}
.header-section p {
color: #606266;
margin: 0;
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.table-section {
background: white;
border-radius: 6px;
overflow: hidden;
}
.product-info {
display: flex;
align-items: center;
}
.product-image-placeholder {
width: 40px;
height: 40px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.product-details {
flex: 1;
}
.product-title {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-tags {
display: flex;
gap: 4px;
}
.budget-info, .roi-info, .period-info, .create-time {
font-size: 12px;
}
.current-budget, .roi-current {
color: #303133;
margin-bottom: 2px;
}
.target-budget, .roi-actual {
color: #909399;
}
.ad-spend {
font-weight: 600;
color: #E6A23C;
margin-bottom: 4px;
}
.spend-link a {
color: #409EFF;
text-decoration: none;
font-size: 12px;
}
.status-info {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.running {
background-color: #67C23A;
}
.status-dot.hosting {
background-color: #409EFF;
}
.status-dot.paused {
background-color: #E6A23C;
}
.status-dot.stopped {
background-color: #F56C6C;
}
.pagination-section {
padding: 20px;
text-align: right;
}
</style>

View File

@@ -1,264 +0,0 @@
<template>
<div class="auto-review-container">
<!-- 顶部筛选和统计栏 -->
<div class="filter-stats-bar">
<div class="left-section">
<span class="section-title">回评记录</span>
<span class="total-count">合计500条 <span class="sub-text">(可查看历史190天)</span></span>
</div>
<div class="right-section">
<div class="date-filters">
<span class="filter-label">回复日期</span>
<div class="date-picker-group">
<el-date-picker
size="small"
type="date"
placeholder="开始日期"
value-format="yyyy-MM-dd"
style="width: 130px;">
</el-date-picker>
<span class="separator"></span>
<el-date-picker
size="small"
type="date"
placeholder="结束日期"
value-format="yyyy-MM-dd"
style="width: 130px;">
</el-date-picker>
</div>
</div>
<div class="view-controls">
<el-button-group>
<el-button size="small" icon="el-icon-s-grid">重置</el-button>
<el-button size="small" icon="el-icon-s-order">回评模板</el-button>
</el-button-group>
</div>
</div>
</div>
<!-- 订单评价列表 -->
<div class="review-table-section">
<el-table :data="reviewData" style="width: 100%" border>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="星级" width="120">
<template slot-scope="scope">
<div class="rating-stars">
<i class="el-icon-star-on" v-for="n in scope.row.rating" :key="n" style="color: #FFAC2D;"></i>
</div>
</template>
</el-table-column>
<el-table-column label="订单编号" width="150" prop="orderNumber"></el-table-column>
<el-table-column label="买家评价" width="200" prop="buyerReview"></el-table-column>
<el-table-column label="商家回评" width="200">
<template slot-scope="scope">
<div>{{ scope.row.merchantReply || '法国专柜同步' }}</div>
</template>
</el-table-column>
<el-table-column label="操作时间" width="150" prop="operationTime"></el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
module.exports = {
name: 'auto-review',
data() {
return {
// 分页相关
currentPage: 1,
pageSize: 10,
total: 500,
// 评价数据
reviewData: [
{
id: 1,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 2,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 3,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 4,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 5,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
}
]
}
},
methods: {
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
},
// 加载数据
loadData() {
// 这里可以添加从后端获取数据的逻辑
console.log('加载数据...');
}
},
mounted() {
this.loadData();
}
}
</script>
<style scoped>
.auto-review-container {
padding: 20px;
background-color: #fff;
height: auto;
min-height: auto;
}
/* 顶部筛选和统计栏样式 */
.filter-stats-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
background-color: #fff;
border-radius: 4px;
border-bottom: 1px solid #EBEEF5;
}
.left-section {
display: flex;
align-items: center;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.total-count {
margin-left: 15px;
font-size: 14px;
color: #606266;
}
.total-count strong {
color: #409EFF;
font-weight: 600;
}
.sub-text {
color: #909399;
font-size: 12px;
margin-left: 4px;
}
.right-section {
display: flex;
align-items: center;
}
.date-filters {
display: flex;
align-items: center;
margin-right: 20px;
background-color: #f5f7fa;
padding: 5px 10px;
border-radius: 4px;
}
.filter-label {
margin-right: 10px;
color: #606266;
font-size: 13px;
}
.date-picker-group {
display: flex;
align-items: center;
}
.separator {
margin: 0 8px;
color: #909399;
}
.view-controls {
display: flex;
align-items: center;
}
.view-controls .el-button-group {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.review-table-section {
background: white;
border-radius: 4px;
overflow: hidden;
}
.rating-stars {
display: flex;
}
.rating-stars i {
margin-right: 2px;
}
.pagination-section {
padding: 15px;
text-align: right;
background-color: #fff;
border-top: 1px solid #ebeef5;
}
</style>

View File

@@ -1,780 +0,0 @@
<template>
<div class="long-product-manage-container">
<!-- 页面头部 -->
<div class="content-header">
<div class="page-title-section">
<h2>较长铺货商品操作记录</h2>
<div class="record-stats">
<span>合计 <strong>500条</strong> (操作记录保存180天)</span>
</div>
</div>
<div class="action-buttons">
<el-button type="primary" icon="el-icon-download" @click="exportRecords">导出记录</el-button>
<el-button type="success" icon="el-icon-refresh" @click="refreshData">刷新数据</el-button>
</div>
</div>
<!-- 筛选工具栏 -->
<div class="filter-toolbar">
<div class="filter-group">
<el-input
placeholder="请输入"
v-model="searchKeyword"
size="small"
style="width: 200px;">
</el-input>
</div>
<div class="filter-group">
<label>修改时间</label>
<el-date-picker
v-model="modifyTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
style="width: 280px;">
</el-date-picker>
</div>
<div class="filter-group">
<label>回溯时间</label>
<el-date-picker
v-model="backtrackTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
style="width: 280px;">
</el-date-picker>
</div>
<el-button type="primary" icon="el-icon-search" size="small" @click="searchRecords">查询</el-button>
<el-button icon="el-icon-refresh-left" size="small" @click="clearFilters">清空</el-button>
</div>
<!-- 操作记录列表 -->
<div class="records-table-section">
<el-table
:data="displayRecords"
style="width: 100%"
:header-cell-style="{background: '#f5f7fa', color: '#606266', fontWeight: '500'}"
:cell-style="{padding: '12px 0'}"
border>
<el-table-column label="商品信息" width="400">
<template slot-scope="scope">
<div class="product-info">
<div class="product-image-placeholder" :style="{ backgroundColor: getProductColor(scope.row.id) }">
<img v-if="scope.row.image" :src="scope.row.image" alt="商品图片">
<span v-else>{{ scope.row.name.substr(0, 1) }}</span>
</div>
<div class="product-details">
<div class="product-name" :title="scope.row.name">{{ scope.row.name }}</div>
<div class="product-specs">{{ scope.row.specs }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="商品ID" width="150" align="center">
<template slot-scope="scope">
<div class="sku-info">
<div class="sku-number" @click.stop="copySKU(scope.row.sku)" title="点击复制 SKU">
{{ scope.row.sku }}
<i class="el-icon-copy-document copy-icon"></i>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作时间" min-width="200">
<template slot-scope="scope">
<div class="time-info">
<div class="time-item">
<span class="time-label">修改时间:</span>
<span class="time-value">{{ scope.row.modifyTime }}</span>
</div>
<div class="time-item">
<span class="time-label">回溯时间:</span>
<span class="time-value">{{ scope.row.backtrackTime }}</span>
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-section">
<div class="pagination-wrapper">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
module.exports = {
name: 'long-product-manage',
data() {
return {
// 筛选相关
searchKeyword: '',
modifyTimeRange: [],
backtrackTimeRange: [],
// 分页相关
currentPage: 1,
pageSize: 10,
total: 500,
// 操作记录数据
recordData: [
{
id: 1,
name: '夏季时尚T恤 - 多色可选舒适透气',
specs: '黑色 M码 纯棉材质',
sku: '250802RCPEQY1D',
modifyTime: '2025-07-01 15:00:00',
backtrackTime: '2025-07-01 15:00:00',
image: null
},
{
id: 2,
name: '智能蓝牙耳机 - 降噪高音质',
specs: '白色 无线版 支持ANC',
sku: '250802RCPEQY1E',
modifyTime: '2025-07-01 14:30:00',
backtrackTime: '2025-07-01 14:30:00',
image: null
},
{
id: 3,
name: '家用收纳盒 - 透明分类整理',
specs: '大号 透明材质 可叠放',
sku: '250802RCPEQY1F',
modifyTime: '2025-07-01 14:00:00',
backtrackTime: '2025-07-01 14:00:00',
image: null
},
{
id: 4,
name: '运动球鞋 - 透气舒适跑步鞋',
specs: '白色 42码 网面材质',
sku: '250802RCPEQY1G',
modifyTime: '2025-07-01 13:30:00',
backtrackTime: '2025-07-01 13:30:00',
image: null
},
{
id: 5,
name: '智能手表 - 多功能运动手环',
specs: '黑色 支持心率监测',
sku: '250802RCPEQY1H',
modifyTime: '2025-07-01 13:00:00',
backtrackTime: '2025-07-01 13:00:00',
image: null
}
],
// 颜色数组
productColors: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
}
},
watch: {
// 监听搜索关键词变化
searchKeyword: {
handler(newVal) {
this.debounceSearch();
},
immediate: false
},
// 监听时间范围变化
modifyTimeRange: {
handler(newVal) {
if (newVal && newVal.length === 2) {
this.filterRecords();
}
},
deep: true
},
backtrackTimeRange: {
handler(newVal) {
if (newVal && newVal.length === 2) {
this.filterRecords();
}
},
deep: true
}
},
computed: {
// 计算当前显示的记录
displayRecords() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.recordData.slice(start, end);
}
},
created() {
// 初始化防抖搜索
this.debounceSearch = this.debounce(this.performSearch, 500);
},
mounted() {
console.log('较长铺货商品管理组件已加载');
},
methods: {
// 查询操作
searchRecords() {
this.$message.info('查询功能开发中...');
},
// 导出记录
exportRecords() {
this.$message.info('导出记录功能开发中...');
},
// 刷新数据
refreshData() {
const loading = this.$loading({
lock: true,
text: '正在刷新数据...',
spinner: 'el-icon-loading'
});
// 模拟异步加载
setTimeout(() => {
loading.close();
this.$message.success('数据已刷新');
// 这里可以添加实际的数据刷新逻辑
}, 1500);
},
// 获取商品图片背景颜色
getProductColor(id) {
return this.productColors[(id - 1) % this.productColors.length];
},
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
},
// 防抖函数
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// 执行搜索
performSearch() {
if (this.searchKeyword.trim()) {
this.$message.info(`搜索: ${this.searchKeyword}`);
// 这里可以添加实际的搜索逻辑
}
},
// 筛选记录
filterRecords() {
this.$message.info('正在按时间范围筛选记录...');
// 这里可以添加实际的筛选逻辑
},
// 清空筛选条件
clearFilters() {
this.searchKeyword = '';
this.modifyTimeRange = [];
this.backtrackTimeRange = [];
this.$message.success('已清空筛选条件');
},
// 打开记录详情
viewRecordDetail(record) {
this.$message.info(`查看记录详情: ${record.name}`);
// 这里可以打开详情对话框
},
// 复制 SKU
copySKU(sku) {
// 使用 Clipboard API 复制到剪贴板
if (navigator.clipboard) {
navigator.clipboard.writeText(sku).then(() => {
this.$message.success('已复制 SKU 到剪贴板');
}).catch(() => {
this.fallbackCopy(sku);
});
} else {
this.fallbackCopy(sku);
}
},
// 备用复制方法
fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
this.$message.success('已复制 SKU 到剪贴板');
} catch (err) {
this.$message.error('复制失败,请手动复制');
}
document.body.removeChild(textArea);
}
}
}
</script>
<style scoped>
/* 主容器 */
.long-product-manage-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
/* 页面头部 */
.content-header {
background: #ffffff;
padding: 20px 24px;
border-radius: 12px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.page-title-section h2 {
color: #303133;
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
}
.record-stats {
color: #606266;
font-size: 14px;
}
.record-stats strong {
color: #409eff;
font-weight: 600;
}
.action-buttons {
display: flex;
gap: 12px;
}
/* 筛选工具栏 */
.filter-toolbar {
background: #fff;
padding: 16px 20px;
border-radius: 8px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-group label {
color: #606266;
font-size: 14px;
white-space: nowrap;
}
/* 记录列表 */
.records-table-section {
background: #ffffff;
border-radius: 12px;
border: 1px solid #e4e7ed;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
/* Element UI 表格样式优化 */
.records-table-section .el-table {
border-radius: 12px;
overflow: hidden;
}
.records-table-section .el-table th {
background: #f5f7fa !important;
color: #606266 !important;
font-weight: 500 !important;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.records-table-section .el-table td {
border-bottom: 1px solid #f0f2f5;
padding: 12px 0;
}
.records-table-section .el-table tbody tr:hover > td {
background-color: #f5f7fa !important;
}
.records-table-section .el-table--border {
border: none;
}
.records-table-section .el-table--border::after {
display: none;
}
.records-table-section .el-table--border th {
border-right: 1px solid #e4e7ed;
}
.records-table-section .el-table--border td {
border-right: 1px solid #f0f2f5;
}
/* 移除旧的单元格样式现在使用Element UI表格 */
.product-info {
display: flex;
align-items: flex-start;
width: 100%;
gap: 12px;
}
.product-image-placeholder {
width: 48px;
height: 48px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
overflow: hidden;
flex-shrink: 0;
margin-top: 2px;
}
.product-image-placeholder img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-details {
flex: 1;
min-width: 0;
max-width: 320px;
padding-top: 2px;
overflow: hidden;
}
.product-name {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
font-weight: 500;
line-height: 1.4;
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
max-width: 300px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.product-specs {
font-size: 12px;
color: #909399;
line-height: 1.3;
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
max-width: 300px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.sku-info {
width: 100%;
padding-top: 2px;
}
.sku-number {
font-size: 14px;
color: #303133;
font-family: 'Courier New', monospace;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.3s;
}
.sku-number:hover {
background-color: #f5f7fa;
color: #409eff;
}
.copy-icon {
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
}
.sku-number:hover .copy-icon {
opacity: 1;
}
.time-info {
width: 100%;
padding-top: 2px;
}
.time-item {
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.time-item:last-child {
margin-bottom: 0;
}
.time-label {
color: #606266;
font-size: 12px;
min-width: 64px;
}
.time-value {
color: #303133;
font-size: 14px;
}
/* 分页 */
.pagination-section {
background: #ffffff;
padding: 20px 24px;
border-radius: 12px;
margin-top: 20px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.pagination-wrapper {
display: flex;
justify-content: center;
}
/* 筛选工具栏 */
.filter-toolbar {
background: #ffffff;
padding: 20px 24px;
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.long-product-manage-container {
padding: 16px;
}
.content-header,
.filter-toolbar,
.pagination-section {
padding: 16px 20px;
}
.product-header {
flex: 0 0 380px;
}
.product-cell {
flex: 0 0 380px;
}
}
@media (max-width: 768px) {
.long-product-manage-container {
padding: 12px;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 16px;
}
.action-buttons {
width: 100%;
justify-content: flex-start;
gap: 8px;
}
.filter-toolbar {
padding: 16px;
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.filter-group {
justify-content: space-between;
width: 100%;
}
.records-table-section {
overflow-x: auto;
}
.table-header,
.table-row {
min-width: 600px;
}
.product-header {
flex: 0 0 280px;
}
.sku-header {
flex: 0 0 150px;
}
.time-header {
min-width: 170px;
}
.product-cell {
flex: 0 0 280px;
}
.sku-cell {
flex: 0 0 150px;
}
.time-cell {
min-width: 170px;
}
.product-image-placeholder {
width: 40px;
height: 40px;
}
.product-name {
font-size: 12px;
max-width: 200px;
}
.product-specs {
font-size: 11px;
max-width: 200px;
}
.sku-number {
font-size: 11px;
}
.time-info {
font-size: 11px;
}
.pagination-section {
padding: 12px;
text-align: center;
}
}
@media (max-width: 480px) {
.action-buttons {
flex-direction: column;
width: 100%;
}
.product-header {
flex: 0 0 220px;
}
.sku-header {
flex: 0 0 120px;
}
.product-cell {
flex: 0 0 220px;
}
.sku-cell {
flex: 0 0 120px;
}
.product-name {
font-size: 11px;
max-width: 160px;
}
.product-specs {
font-size: 10px;
max-width: 160px;
}
.sku-number {
font-size: 10px;
}
.time-info {
font-size: 10px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,493 +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>ERP系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
}
body {
background-color: #f5f5f5;
color: #333;
}
.container {
display: flex;
height: 100vh;
}
.sidebar {
width: 200px;
background-color: #fff;
border-right: 1px solid #e0e0e0;
padding: 15px 0;
overflow-y: auto;
}
.sidebar-header {
padding: 0 15px 15px;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-header h2 {
font-size: 16px;
color: #333;
margin-bottom: 5px;
}
.platform-list {
list-style: none;
margin-top: 15px;
}
.platform-item {
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.platform-item:hover {
background-color: #f0f0f0;
}
.platform-item.active {
background-color: #e6f7ff;
border-right: 3px solid #1890ff;
}
.platform-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
.platform-name {
font-size: 14px;
}
.main-content {
flex: 1;
padding: 15px;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.header-title {
font-size: 18px;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 10px;
}
.card-container {
display: flex;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.card {
background-color: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
flex: 1;
min-width: 200px;
display: flex;
align-items: center;
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.card-content h3 {
font-size: 14px;
margin-bottom: 5px;
color: #666;
}
.card-content p {
font-size: 16px;
font-weight: bold;
}
.tabs {
display: flex;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 15px;
}
.tab {
padding: 10px 15px;
cursor: pointer;
font-size: 14px;
position: relative;
}
.tab.active {
color: #1890ff;
}
.tab.active::after {
content: "";
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: #1890ff;
}
.table-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px 15px;
text-align: left;
font-size: 14px;
border-bottom: 1px solid #e0e0e0;
}
th {
background-color: #f5f5f5;
font-weight: 500;
}
tr:hover {
background-color: #f9f9f9;
}
.status {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
}
.status-processing {
background-color: #e6f7ff;
color: #1890ff;
}
.status-completed {
background-color: #f6ffed;
color: #52c41a;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 15px;
align-items: center;
}
.page-item {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
margin: 0 4px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.page-item:hover {
background-color: #f0f0f0;
}
.page-item.active {
background-color: #1890ff;
color: #fff;
}
.action-btn {
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #d9d9d9;
background-color: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.action-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
.action-btn-primary {
background-color: #1890ff;
border-color: #1890ff;
color: #fff;
}
.action-btn-primary:hover {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<div class="sidebar-header">
<h2>我的工具箱</h2>
</div>
<ul class="platform-list">
<li class="platform-item active">
<div class="platform-icon">R</div>
<div class="platform-name">Rakuten</div>
</li>
<li class="platform-item">
<div class="platform-icon">A</div>
<div class="platform-name">Amazon</div>
</li>
<li class="platform-item">
<div class="platform-icon">O</div>
<div class="platform-name">OZON</div>
</li>
<li class="platform-item">
<div class="platform-icon">T</div>
<div class="platform-name">淘宝网</div>
</li>
<li class="platform-item">
<div class="platform-icon">J</div>
<div class="platform-name">京东</div>
</li>
<li class="platform-item">
<div class="platform-icon">P</div>
<div class="platform-name">拼多多</div>
</li>
</ul>
</div>
<div class="main-content">
<div class="header">
<div class="header-title">Shopee 工具箱</div>
<div class="header-actions">
<button class="action-btn">刷新</button>
<button class="action-btn action-btn-primary">导出数据</button>
</div>
</div>
<div class="card-container">
<div class="card">
<div class="card-icon">Ad</div>
<div class="card-content">
<h3>广告投放情况</h3>
<p>5个广告自动化投放中</p>
</div>
</div>
<div class="card">
<div class="card-icon">A</div>
<div class="card-content">
<h3>自动回评</h3>
<p>按照自动回复设置回复</p>
</div>
</div>
<div class="card">
<div class="card-icon">T</div>
<div class="card-content">
<h3>同时特卖自动化</h3>
<p>按照特卖时间自动上下架</p>
</div>
</div>
<div class="card">
<div class="card-icon">P</div>
<div class="card-content">
<h3>批量修改商品</h3>
<p>按模板批量修改商品信息</p>
</div>
</div>
</div>
<div class="tabs">
<div class="tab active">列表查看</div>
<div class="tab">广告设置</div>
<div class="tab">运行中</div>
<div class="tab">已结束</div>
<div class="tab">统计</div>
<div class="tab">任务中</div>
<div class="tab">已完成</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th width="40"><input type="checkbox"></th>
<th>商品信息</th>
<th>每日预算</th>
<th>目标ROI</th>
<th>RO设置周期</th>
<th>RO自动配置</th>
<th>剩余时间/预计时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox"></td>
<td>
<div>商品超级高品质超级商品品牌</div>
<div>商品ID: 554677</div>
</td>
<td>NT$ 100</td>
<td>5</td>
<td>7天</td>
<td>3</td>
<td>07-09 17:00</td>
<td>
<span class="status status-processing">任务中</span>
</td>
</tr>
<tr>
<td><input type="checkbox"></td>
<td>
<div>商品超级高品质超级商品品牌</div>
<div>商品ID: 554677</div>
</td>
<td>NT$ 100</td>
<td>5</td>
<td>7天</td>
<td>3</td>
<td>07-09 17:00</td>
<td>
<span class="status status-processing">任务中</span>
</td>
</tr>
<tr>
<td><input type="checkbox"></td>
<td>
<div>商品超级高品质超级商品品牌</div>
<div>商品ID: 554677</div>
</td>
<td>NT$ 100</td>
<td>5</td>
<td>7天</td>
<td>3</td>
<td>07-09 17:00</td>
<td>
<span class="status status-completed">已完成</span>
</td>
</tr>
<tr>
<td><input type="checkbox"></td>
<td>
<div>商品超级高品质超级商品品牌</div>
<div>商品ID: 554677</div>
</td>
<td>NT$ 100</td>
<td>5</td>
<td>7天</td>
<td>3</td>
<td>07-09 17:00</td>
<td>
<span class="status status-completed">已完成</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="page-item active">1</div>
<div class="page-item">2</div>
<div class="page-item">3</div>
<div class="page-item">4</div>
<div class="page-item">5</div>
<div class="page-item">...</div>
<div class="page-item">10</div>
</div>
</div>
</div>
<script>
// 与Java交互的JavaScript代码
function receiveFromJava(message) {
console.log("从Java收到消息:", message);
// 处理来自Java的消息
}
// 向Java发送消息的示例
function sendToJava(message) {
if (window.javaConnector) {
window.javaConnector.sendToJava(message);
} else {
console.error("javaConnector未定义");
}
}
// 页面加载完成后初始化
document.addEventListener("DOMContentLoaded", function() {
console.log("页面已加载完成");
// 添加平台项点击事件
const platformItems = document.querySelectorAll(".platform-item");
platformItems.forEach(item => {
item.addEventListener("click", function() {
platformItems.forEach(i => i.classList.remove("active"));
this.classList.add("active");
const platformName = this.querySelector(".platform-name").textContent;
sendToJava("选择平台: " + platformName);
});
});
// 添加标签页点击事件
const tabs = document.querySelectorAll(".tab");
tabs.forEach(tab => {
tab.addEventListener("click", function() {
tabs.forEach(t => t.classList.remove("active"));
this.classList.add("active");
sendToJava("选择标签页: " + this.textContent);
});
});
// 添加操作按钮点击事件
const actionBtns = document.querySelectorAll(".action-btn");
actionBtns.forEach(btn => {
btn.addEventListener("click", function() {
sendToJava("点击按钮: " + this.textContent);
});
});
});
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,157 +0,0 @@
/**
* 统一的API请求工具和错误处理
* 极简化版本,专注于核心功能
*/
class ApiUtils {
constructor() {
this.defaultTimeout = 30000; // 30秒超时
}
// 统一的错误消息映射
getErrorMessage(error) {
const errorMap = {
// 网络相关错误
'network error': '网络连接失败',
'timeout': '请求超时',
'connection refused': '服务连接失败',
'cors': '跨域请求失败',
// 文件相关错误
'file format error': '文件格式错误',
'file too large': '文件过大',
'invalid file type': '文件类型不正确',
'no data found': '未找到有效数据',
// 服务器相关错误
'server error': '服务器繁忙',
'internal server error': '系统内部错误',
'forbidden': '操作被拒绝',
'not found': '资源不存在',
'unauthorized': '权限不足',
// 业务相关错误
'asin': 'ASIN格式错误',
'proxy': '代理连接异常',
'amazon': 'Amazon服务异常',
'rakuten': '乐天服务异常',
'shopee': 'Shopee服务异常',
'version check': '版本检查失败',
'update': '更新失败',
// JSON相关错误
'jsonnode': '数据解析失败',
'cannot invoke': '数据处理异常',
'null': '数据为空',
'parsing': '数据格式错误'
};
const errorMsg = error?.message || error || '';
const lowerMsg = errorMsg.toLowerCase();
// 查找匹配的错误类型
for (const [key, value] of Object.entries(errorMap)) {
if (lowerMsg.includes(key)) {
return value;
}
}
// 如果是纯英文,返回通用提示
if (/^[a-zA-Z\s\d_.\-"():]+$/.test(errorMsg)) {
return '操作失败,请稍后重试';
}
return errorMsg || '未知错误';
}
async request(url, options = {}) {
const config = {
timeout: options.timeout || this.defaultTimeout,
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
};
const response = await httpClient(url, config).catch(error => {
throw new Error(this.getErrorMessage(error));
});
if (response.data?.code !== undefined && !(response.data?.code === 0 || response.data?.code === 200)) {
throw new Error(this.getErrorMessage(response.data?.msg || '请求失败'));
}
return response.data?.data !== undefined ? response.data.data : response.data;
}
// 性能优化:直接返回数据,减少包装层级
async get(url, params = {}) {
return this.request(url, { method: 'GET', params });
}
async post(url, data = {}) {
return this.request(url, { method: 'POST', data });
}
async upload(url, formData) {
return this.request(url, {
method: 'POST',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
});
}
// 显示成功消息
showSuccess(message) {
if (window.Vue && window.Vue.prototype.$message) {
window.Vue.prototype.$message.success(message);
} else {
console.log('✅ ' + message);
}
}
// 显示错误消息
showError(error) {
const message = this.getErrorMessage(error);
if (window.Vue && window.Vue.prototype.$message) {
window.Vue.prototype.$message.error(message);
} else {
console.error('❌ ' + message);
}
}
// 显示警告消息
showWarning(message) {
if (window.Vue && window.Vue.prototype.$message) {
window.Vue.prototype.$message.warning(message);
} else {
console.warn('⚠️ ' + message);
}
}
// 显示信息消息
showInfo(message) {
if (window.Vue && window.Vue.prototype.$message) {
window.Vue.prototype.$message.info(message);
} else {
console.info(' ' + message);
}
}
// 性能优化:内联验证逻辑,减少函数调用开销
validateFile(file, options = {}) {
const { maxSize = 10, allowedTypes = ['.xlsx', '.xls'] } = options;
const fileName = file.name.toLowerCase();
if (!allowedTypes.some(type => fileName.endsWith(type))) {
throw new Error(`只支持 ${allowedTypes.join('、')} 格式文件`);
}
if (file.size > maxSize * 1024 * 1024) {
throw new Error(`文件大小不能超过 ${maxSize}MB`);
}
return true;
}
}
// 创建全局实例
window.apiUtils = new ApiUtils();

View File

@@ -1,86 +0,0 @@
/**
* 亚马逊平台API封装
* 专门处理亚马逊相关的所有接口调用
*/
class AmazonAPI {
/**
* 批量获取产品信息
* @param {Array} asinList - ASIN列表
* @param {String} batchId - 批次ID
*/
async getProductsBatch(asinList, batchId) {
return await apiUtils.post('/api/amazon/products/batch', {
asinList,
batchId
});
}
/**
* 获取最新产品数据
*/
async getLatestProducts() {
return await apiUtils.get('/api/amazon/products/latest');
}
/**
* 根据批次ID获取产品数据
* @param {String} batchId - 批次ID
*/
async getProductsByBatch(batchId) {
return await apiUtils.get(`/api/amazon/products/batch/${batchId}`);
}
/**
* 更新产品信息
* @param {Object} productData - 产品数据
*/
async updateProduct(productData) {
return await apiUtils.post('/api/amazon/products/update', productData);
}
/**
* 删除产品
* @param {String} productId - 产品ID
*/
async deleteProduct(productId) {
return await apiUtils.post('/api/amazon/products/delete', { id: productId });
}
/**
* 导出Excel
* @param {Array} products - 产品列表
* @param {Object} options - 导出选项
*/
async exportToExcel(products, options = {}) {
return await apiUtils.post('/api/amazon/export', {
products,
...options
});
}
/**
* 获取产品统计信息
*/
async getProductStats() {
return await apiUtils.get('/api/amazon/stats');
}
/**
* 搜索产品
* @param {Object} searchParams - 搜索参数
*/
async searchProducts(searchParams) {
return await apiUtils.get('/api/amazon/products/search', searchParams);
}
/**
* 打开跟卖精灵
*/
async openGenmaiSpirit() {
return await apiUtils.post('/api/genmai/open');
}
}
// 创建全局实例
window.amazonAPI = new AmazonAPI();

View File

@@ -1,152 +0,0 @@
/**
* 通用API封装
* 处理版本更新、系统监控等通用功能
*/
class CommonAPI {
/**
* 检查版本更新
*/
async checkVersion() {
return await apiUtils.get('/api/update/check');
}
/**
* 获取当前版本信息
*/
async getVersion() {
return await apiUtils.get('/api/update/version');
}
/**
* 自动更新
* @param {Object} updateData - 更新数据
*/
async autoUpdate(updateData) {
return await apiUtils.post('/api/update/auto-update', updateData);
}
/**
* 获取下载进度
*/
async getDownloadProgress() {
return await apiUtils.get('/api/update/progress');
}
/**
* 重置下载状态
*/
async resetDownload() {
return await apiUtils.post('/api/update/reset');
}
/**
* 取消下载
*/
async cancelDownload() {
return await apiUtils.post('/api/update/cancel');
}
/**
* 安装更新
*/
async installUpdate() {
return await apiUtils.post('/api/update/install');
}
/**
* 跳过版本
* @param {String} version - 版本号
*/
async skipVersion(version) {
return await apiUtils.post('/api/update/skip-version', { version });
}
/**
* 客户端状态上报
* @param {Object} statusData - 状态数据
*/
async reportClientStatus(statusData) {
return await apiUtils.post('/api/monitor/client/status', statusData);
}
/**
* 获取系统信息
*/
async getSystemInfo() {
return await apiUtils.get('/api/system/info');
}
/**
* 获取服务器状态
*/
async getServerStatus() {
return await apiUtils.get('/api/monitor/server');
}
/**
* 上传错误日志
* @param {Object} errorData - 错误数据
*/
async uploadErrorLog(errorData) {
return await apiUtils.post('/api/logs/error', errorData);
}
/**
* 获取公告信息
*/
async getNotifications() {
return await apiUtils.get('/api/notifications');
}
/**
* 标记公告已读
* @param {String} notificationId - 公告ID
*/
async markNotificationRead(notificationId) {
return await apiUtils.post('/api/notifications/read', {
id: notificationId
});
}
/**
* 获取用户配置
*/
async getUserConfig() {
return await apiUtils.get('/api/config/user');
}
/**
* 保存用户配置
* @param {Object} config - 配置数据
*/
async saveUserConfig(config) {
return await apiUtils.post('/api/config/user', config);
}
/**
* 重置配置
*/
async resetConfig() {
return await apiUtils.post('/api/config/reset');
}
/**
* 获取帮助文档
* @param {String} section - 文档章节
*/
async getHelpDoc(section) {
return await apiUtils.get(`/api/help/${section}`);
}
/**
* 提交反馈
* @param {Object} feedback - 反馈数据
*/
async submitFeedback(feedback) {
return await apiUtils.post('/api/feedback', feedback);
}
}
// 创建全局实例
window.commonAPI = new CommonAPI();

View File

@@ -1,30 +0,0 @@
// 设备管理 API - 通过本地代理转发到 ruoyi-admin
(function () {
const api = window.apiUtils;
const base = '/api/device'; // 由 DeviceProxyController 转发到 /monitor/device
async function getQuota(username) {
const res = await api.get(`${base}/quota?username=${username}`);
return res && res.limit !== undefined ? res : (res?.data || res || {});
}
async function list(username) {
const res = await api.get(`${base}/list?username=${username}`);
return Array.isArray(res) ? res : (res?.data || []);
}
async function register(payload) {
return api.post(`${base}/register`, payload);
}
async function remove(payload) {
return api.post(`${base}/remove`, payload);
}
async function heartbeat(payload) {
return api.post(`${base}/heartbeat`, payload);
}
window.deviceAPI = { getQuota, list, register, remove, heartbeat };
})();

View File

@@ -1,63 +0,0 @@
/**
* 乐天平台API封装
* 专门处理乐天相关的所有接口调用
*/
class RakutenAPI {
/**
* 获取乐天商品数据(支持文件上传和单个店铺查询)
* @param {Object} params - 参数对象
* @param {File} params.file - Excel文件可选
* @param {String} params.shopName - 店铺名(可选)
* @param {String} params.batchId - 批次ID可选
*/
async getProducts(params = {}) {
const formData = new FormData();
if (params.file) {
formData.append('file', params.file);
}
if (params.shopName) {
formData.append('shopName', params.shopName);
}
if (params.batchId) {
formData.append('batchId', params.batchId);
}
return await apiUtils.upload('/api/rakuten/products', formData);
}
/**
* 1688识图搜索
* @param {String} imageUrl - 图片URL
* @param {String} sessionId - 会话ID (可选)
*/
async search1688(imageUrl, sessionId) {
const params = { imageUrl };
if (sessionId) {
params.sessionId = sessionId;
}
return await apiUtils.post('/api/rakuten/search1688', params);
}
/**
* 获取最新的乐天商品数据
*/
async getLatestProducts() {
return await apiUtils.get('/api/rakuten/products/latest');
}
/**
* 导出并保存Excel文件到桌面JavaFX专用
* @param {Object} exportData - 导出数据
*/
async exportAndSave(exportData) {
return await apiUtils.post('/api/rakuten/export-and-save', exportData);
}
}
// 创建全局实例
window.rakutenAPI = new RakutenAPI();

View File

@@ -1,130 +0,0 @@
/**
* Shopee平台API封装
* 专门处理Shopee相关的所有接口调用
*/
class ShopeeAPI {
/**
* 获取广告托管数据
* @param {Object} params - 查询参数
*/
async getAdHosting(params = {}) {
return await apiUtils.get('/api/shopee/ad-hosting', params);
}
/**
* 编辑广告托管
* @param {Object} adData - 广告数据
*/
async editAdHosting(adData) {
return await apiUtils.post('/api/shopee/ad-hosting/edit', adData);
}
/**
* 删除广告托管
* @param {String} id - 广告ID
*/
async deleteAdHosting(id) {
return await apiUtils.post('/api/shopee/ad-hosting/delete', { id });
}
/**
* 获取评价列表
* @param {Object} params - 查询参数
*/
async getReviews(params = {}) {
return await apiUtils.get('/api/shopee/reviews', params);
}
/**
* 回复评价
* @param {Object} replyData - 回复数据
*/
async replyToReview(replyData) {
return await apiUtils.post('/api/shopee/review/reply', replyData);
}
/**
* 删除评价
* @param {String} id - 评价ID
*/
async deleteReview(id) {
return await apiUtils.post('/api/shopee/review/delete', { id });
}
/**
* 保存回评模板
* @param {String} template - 模板内容
*/
async saveReplyTemplate(template) {
return await apiUtils.post('/api/shopee/reply-template', { template });
}
/**
* 获取回评模板
*/
async getReplyTemplate() {
return await apiUtils.get('/api/shopee/reply-template');
}
/**
* 获取闪购活动列表
* @param {Object} params - 查询参数
*/
async getFlashSales(params = {}) {
return await apiUtils.get('/api/shopee/flash-sales', params);
}
/**
* 创建闪购活动
* @param {Object} flashSaleData - 闪购数据
*/
async createFlashSale(flashSaleData) {
return await apiUtils.post('/api/shopee/flash-sales/create', flashSaleData);
}
/**
* 更新闪购活动
* @param {Object} flashSaleData - 闪购数据
*/
async updateFlashSale(flashSaleData) {
return await apiUtils.post('/api/shopee/flash-sales/update', flashSaleData);
}
/**
* 删除闪购活动
* @param {String} id - 闪购ID
*/
async deleteFlashSale(id) {
return await apiUtils.post('/api/shopee/flash-sales/delete', { id });
}
/**
* 获取商品管理列表
* @param {Object} params - 查询参数
*/
async getProductManagement(params = {}) {
return await apiUtils.get('/api/shopee/products', params);
}
/**
* 批量更新商品
* @param {Array} products - 商品列表
*/
async batchUpdateProducts(products) {
return await apiUtils.post('/api/shopee/products/batch-update', {
products
});
}
/**
* 导出Shopee数据
* @param {Object} exportParams - 导出参数
*/
async exportData(exportParams) {
return await apiUtils.post('/api/shopee/export', exportParams);
}
}
// 创建全局实例
window.shopeeAPI = new ShopeeAPI();

View File

@@ -1,69 +0,0 @@
/**
* 斑马平台API封装
* 专门处理斑马相关的所有接口调用
*/
class ZebraAPI {
/**
* 获取订单数据(分页)
* @param {Object} params - 查询参数
*/
async getOrders(params = {}) {
return await apiUtils.get('/api/banma/orders', params);
}
/**
* 根据批次ID获取订单数据
* @param {String} batchId - 批次ID
*/
async getOrdersByBatch(batchId) {
return await apiUtils.get(`/api/banma/orders/batch/${batchId}`);
}
/**
* 获取最新的订单数据
*/
async getLatestOrders() {
return await apiUtils.get('/api/banma/orders/latest');
}
/**
* 获取店铺列表
*/
async getShops() {
return await apiUtils.get('/api/banma/shops');
}
/**
* 刷新认证Token
*/
async refreshToken() {
return await apiUtils.post('/api/banma/refresh-token');
}
/**
* 导出订单数据到Excel
* @param {Object} exportData - 导出数据
*/
async exportAndSaveOrders(exportData) {
return await apiUtils.post('/api/banma/export-and-save', exportData);
}
/**
* 获取订单统计信息
*/
async getOrderStats() {
return await apiUtils.get('/api/banma/orders/stats');
}
/**
* 搜索订单
* @param {Object} searchParams - 搜索参数
*/
async searchOrders(searchParams) {
return await apiUtils.get('/api/banma/orders/search', searchParams);
}
}
// 创建全局实例
window.zebraAPI = new ZebraAPI();

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +0,0 @@
/**
* HTTP Client Utility - Axios wrapper with error handling and logging
*/
(function() {
// Create axios instance with default config
const httpClient = axios.create({
baseURL: 'http://localhost:8081', // Set the base URL for all requests
timeout: 0, // 无限时间 - 永不超时
// 针对JavaFX WebView的特殊配置
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
},
// 对于文件上传禁用自动设置Content-Type
transformRequest: [function (data, headers) {
// 如果是FormData让浏览器自动设置Content-Type
if (data instanceof FormData) {
delete headers['Content-Type'];
return data;
}
return axios.defaults.transformRequest[0](data, headers);
}]
});
// Request interceptor for logging
httpClient.interceptors.request.use(
config => {
console.log(`🚀 REQUEST: ${config.method.toUpperCase()} ${config.baseURL}${config.url}`, config);
return config;
},
error => {
console.error('❌ Request Error:', error);
return Promise.reject(error);
}
);
// Response interceptor for logging
httpClient.interceptors.response.use(
response => {
console.log(`✅ RESPONSE: ${response.config.method.toUpperCase()} ${response.config.url}`, response.data);
return response;
},
error => {
if (error.response) {
// Server responded with a status code outside of 2xx range
console.error(`❌ Response Error: ${error.response.status}`, error.response.data);
} else if (error.request) {
// Request was made but no response was received
console.error('❌ Network Error: No response received', error.request);
} else {
// Something happened in setting up the request
console.error('❌ Request Config Error:', error.message);
}
return Promise.reject(error);
}
);
// Export the httpClient to global scope
window.httpClient = httpClient;
})();

View File

@@ -1,282 +0,0 @@
/**
* 数据处理服务
* 统一处理数据格式化、验证、转换等功能
*/
class DataService {
/**
* 格式化产品数据
* @param {Array} products - 原始产品数据
* @param {String} platform - 平台类型
* @returns {Array} 格式化后的产品数据
*/
// 性能优化:直接返回结果,减少中间变量
formatProductData(products, platform = 'amazon') {
if (!Array.isArray(products)) return [];
const formatters = {
'amazon': this.formatAmazonProduct.bind(this),
'rakuten': this.formatRakutenProduct.bind(this),
'shopee': this.formatShopeeProduct.bind(this)
};
const formatter = formatters[platform];
return formatter ? products.map(formatter) : products;
}
/**
* 格式化亚马逊产品数据
* @param {Object} product - 产品数据
*/
formatAmazonProduct(product) {
return {
...product,
asin: product.asin || '无数据',
seller: product.seller || '无货',
shipper: product.shipper || '',
price: product.price || '无货',
title: product.title || '无标题',
createTime: this.formatTime(product.createTime),
updateTime: this.formatTime(product.updateTime)
};
}
/**
* 格式化乐天产品数据
* @param {Object} product - 产品数据
*/
formatRakutenProduct(product) {
return {
...product,
productUrl: product.productUrl || '',
imgUrl: product.imgUrl || '',
productTitle: product.productTitle || '无标题',
price: product.price || '无价格',
ranking: product.ranking || '无排名',
shopName: product.shopName || product.originalShopName || '无店铺',
image1688Url: product.image1688Url || '',
detailUrl1688: product.detailUrl1688 || '',
};
}
/**
* 格式化Shopee产品数据
* @param {Object} product - 产品数据
*/
formatShopeeProduct(product) {
return {
...product,
title: product.title || '无标题',
price: product.price || '无价格',
sales: product.sales || 0,
rating: product.rating || 0,
shopName: product.shopName || '无店铺'
};
}
/**
* 验证产品数据
* @param {Object} product - 产品数据
* @param {String} platform - 平台类型
* @returns {Object} 验证结果 {valid: boolean, errors: Array}
*/
validateProductData(product, platform = 'amazon') {
const errors = [];
if (!product || typeof product !== 'object') {
errors.push('产品数据无效');
return { valid: false, errors };
}
switch (platform) {
case 'amazon':
if (!product.asin || product.asin.trim() === '') {
errors.push('ASIN不能为空');
}
break;
case 'rakuten':
if (!product.productUrl) {
errors.push('产品URL不能为空');
}
if (!product.productTitle) {
errors.push('产品标题不能为空');
}
break;
case 'shopee':
if (!product.title) {
errors.push('商品标题不能为空');
}
break;
}
return {
valid: errors.length === 0,
errors
};
}
/**
* 去重产品数据
* @param {Array} products - 产品数据数组
* @param {String} keyField - 去重的关键字段
* @returns {Array} 去重后的产品数据
*/
// 性能优化:简化去重逻辑,提高可读性
removeDuplicateProducts(products, keyField = 'id') {
if (!Array.isArray(products)) return [];
const seen = new Set();
return products.filter(product => {
const key = product[keyField];
return key && !seen.has(key) && seen.add(key);
});
}
/**
* 分页数据
* @param {Array} data - 数据数组
* @param {Number} page - 页码从1开始
* @param {Number} pageSize - 页面大小
* @returns {Object} 分页结果 {data: Array, total: Number, hasMore: Boolean}
*/
paginate(data, page = 1, pageSize = 10) {
if (!Array.isArray(data)) {
return { data: [], total: 0, hasMore: false };
}
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedData = data.slice(start, end);
return {
data: paginatedData,
total: data.length,
hasMore: end < data.length,
currentPage: page,
totalPages: Math.ceil(data.length / pageSize)
};
}
/**
* 搜索过滤数据
* @param {Array} data - 数据数组
* @param {String} searchText - 搜索文本
* @param {Array} searchFields - 搜索字段列表
* @returns {Array} 过滤后的数据
*/
// 性能优化:内联逻辑,减少函数调用层级
searchFilter(data, searchText, searchFields = ['title', 'name']) {
if (!Array.isArray(data) || !searchText?.trim()) return data;
const lowerSearchText = searchText.toLowerCase().trim();
return data.filter(item =>
searchFields.some(field => {
const value = item[field];
return value && value.toString().toLowerCase().includes(lowerSearchText);
})
);
}
/**
* 状态过滤数据
* @param {Array} data - 数据数组
* @param {String} status - 状态值
* @param {String} statusField - 状态字段名
* @returns {Array} 过滤后的数据
*/
statusFilter(data, status, statusField = 'status') {
if (!Array.isArray(data) || !status || status === '全部' || status === '全部商品') {
return data;
}
return data.filter(item => item[statusField] === status);
}
/**
* 格式化时间
* @param {String|Date|Object} time - 时间对象
* @returns {String} 格式化后的时间字符串
*/
formatTime(time) {
if (!time) return '无数据';
if (typeof time === 'object' && time.date) {
return time.date;
}
if (typeof time === 'string') {
return time.replace(/T/, ' ').slice(0, 16);
}
if (time instanceof Date) {
return time.toISOString().replace(/T/, ' ').slice(0, 16);
}
return '无数据';
}
/**
* 格式化价格
* @param {String|Number} price - 价格
* @param {String} currency - 货币符号
* @returns {String} 格式化后的价格
*/
formatPrice(price, currency = '¥') {
if (!price || price === '无价格' || price === '无货') {
return '无货';
}
const numPrice = parseFloat(price.toString().replace(/[^0-9.]/g, ''));
if (isNaN(numPrice)) {
return '无货';
}
return `${currency} ${numPrice.toFixed(2)}`;
}
/**
* 计算统计信息
* @param {Array} data - 数据数组
* @param {String} field - 统计字段
* @returns {Object} 统计结果
*/
calculateStats(data, field) {
if (!Array.isArray(data) || data.length === 0) {
return {
count: 0,
min: 0,
max: 0,
avg: 0,
sum: 0
};
}
const values = data
.map(item => parseFloat(item[field]))
.filter(val => !isNaN(val));
if (values.length === 0) {
return {
count: 0,
min: 0,
max: 0,
avg: 0,
sum: 0
};
}
const sum = values.reduce((a, b) => a + b, 0);
return {
count: values.length,
min: Math.min(...values),
max: Math.max(...values),
avg: sum / values.length,
sum: sum
};
}
}
// 创建全局实例
window.dataService = new DataService();

View File

@@ -1,206 +0,0 @@
/**
* 文件处理服务
* 统一处理文件上传、验证、解析、导出等功能
*/
class FileService {
/**
* 验证文件
* @param {File} file - 文件对象
* @param {Object} options - 验证选项
* @param {Number} options.maxSize - 最大文件大小(MB)默认10MB
* @param {Array} options.allowedTypes - 允许的文件类型,默认['.xlsx', '.xls']
*/
// 性能优化:内联判断逻辑,减少变量声明
validateFile(file, options = {}) {
const { maxSize = 10, allowedTypes = ['.xlsx', '.xls'] } = options;
if (!file) throw new Error('请选择文件');
const fileName = file.name.toLowerCase();
if (!allowedTypes.some(type => fileName.endsWith(type))) {
throw new Error(`只支持 ${allowedTypes.join('、')} 格式文件`);
}
if (file.size > maxSize * 1048576) { // 1MB = 1048576 bytes
throw new Error(`文件大小不能超过 ${maxSize}MB`);
}
return true;
}
/**
* 解析Excel文件并提取ASIN列表
* @param {File} file - Excel文件
* @returns {Promise<Array>} ASIN列表
*/
// 性能优化直接处理错误减少嵌套try-catch
async parseExcelForASIN(file) {
const data = await this.readFileAsArrayBuffer(file);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(data);
const worksheet = workbook.worksheets[0];
const asinList = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) { // 跳过标题行
const asin = row.getCell(1).text.trim();
if (asin) asinList.push(asin);
}
});
if (asinList.length === 0) {
throw new Error('Excel文件中未找到有效的ASIN');
}
return asinList;
}
/**
* 解析Excel文件并提取店铺名列表
* @param {File} file - Excel文件
* @returns {Promise<Array>} 店铺名列表
*/
// 性能优化复用parseExcelForASIN逻辑减少代码重复
async parseExcelForShopNames(file) {
const data = await this.readFileAsArrayBuffer(file);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(data);
const worksheet = workbook.worksheets[0];
const shopNames = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) {
const shopName = row.getCell(1).text.trim();
if (shopName) shopNames.push(shopName);
}
});
if (shopNames.length === 0) {
throw new Error('Excel文件中未找到有效的店铺名');
}
return shopNames;
}
/**
* 创建Excel工作簿
* @param {String} sheetName - 工作表名称
* @param {Array} headers - 表头数组
* @param {Array} data - 数据数组
* @returns {Promise<ExcelJS.Workbook>} Excel工作簿
*/
async createExcelWorkbook(sheetName, headers, data) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(sheetName);
// 设置表头
worksheet.columns = headers.map((header, index) => ({
header: header.label || header,
key: header.key || `col_${index}`,
width: header.width || 15
}));
// 添加数据
data.forEach(row => {
worksheet.addRow(row);
});
// 设置表头样式
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).alignment = {
vertical: 'middle',
horizontal: 'center'
};
return workbook;
}
/**
* 导出Excel文件浏览器下载
* @param {ExcelJS.Workbook} workbook - Excel工作簿
* @param {String} fileName - 文件名
*/
async exportExcelFile(workbook, fileName) {
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* 保存Excel文件到桌面JavaFX环境
* @param {ExcelJS.Workbook} workbook - Excel工作簿
* @param {String} fileName - 文件名
* @returns {String|null} 保存路径
*/
async saveExcelToDesktop(workbook, fileName) {
try {
const buffer = await workbook.xlsx.writeBuffer();
const base64Data = btoa(String.fromCharCode(...new Uint8Array(buffer)));
if (window.javaConnector) {
return window.javaConnector.saveExcelFile(base64Data, fileName);
} else {
// 降级到浏览器下载
await this.exportExcelFile(workbook, fileName);
return null;
}
} catch (error) {
throw new Error(`保存文件失败: ${error.message}`);
}
}
/**
* 读取文件为ArrayBuffer
* @param {File} file - 文件对象
* @returns {Promise<ArrayBuffer>}
*/
readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(new Error('读取文件失败'));
reader.readAsArrayBuffer(file);
});
}
/**
* 读取文件为Base64
* @param {File} file - 文件对象
* @returns {Promise<String>}
*/
readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => resolve(e.target.result.split(',')[1]);
reader.onerror = e => reject(new Error('读取文件失败'));
reader.readAsDataURL(file);
});
}
/**
* 格式化文件大小
* @param {Number} bytes - 字节数
* @returns {String} 格式化后的大小
*/
formatFileSize(bytes) {
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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
// 创建全局实例
window.fileService = new FileService();

View File

@@ -1,55 +0,0 @@
(function(){
function parseComponent(name, source){
var container = document.createElement('div');
container.innerHTML = source;
var tpl = container.querySelector('template');
var style = container.querySelector('style');
var script = container.querySelector('script');
if (style && style.textContent) {
var styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.textContent = style.textContent;
document.head.appendChild(styleTag);
}
var options = {};
if (script && script.textContent) {
var code = script.textContent.trim();
// support export default ...
if (/^export\s+default/.test(code)) {
code = code.replace(/^export\s+default/, 'return');
} else if (/module\.exports\s*=/.test(code)) {
code = code.replace(/module\.exports\s*=/, 'return');
} else if (!/^return\s+/.test(code)) {
code = 'return (' + code + ')';
}
try {
options = (new Function(code))() || {};
} catch (e) {
console.error('SFC script eval failed for', name, e);
options = {};
}
}
options = options || {};
options.template = tpl ? tpl.innerHTML : '<div></div>';
Vue.component(name, options);
}
function loadOne(def){
// 使用axios替代fetch
return axios.get(def.url, { cache: false })
.then(function(response){ return response.data; })
.then(function(text){ parseComponent(def.name, text); })
.catch(function(error) {
console.error('Failed to load component:', def.name, error);
return Promise.reject(error);
});
}
window.loadSfcComponents = function(defs){
if (!defs || !defs.length) return Promise.resolve();
return Promise.all(defs.map(loadOne));
};
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ public class VersionController extends BaseController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String VERSION_REDIS_KEY = "erp:client:version";
private static final String DOWNLOAD_URL_REDIS_KEY = "erp:client:download:url";
private static final String DOWNLOAD_URL_REDIS_KEY = "erp:client:url";
/**
* 检查版本更新
@@ -85,23 +85,12 @@ public class VersionController extends BaseController {
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
@RequestParam("downloadUrl") String downloadUrl) {
try {
if (StringUtils.isEmpty(version)) {
return AjaxResult.error("版本号不能为空");
}
if (StringUtils.isEmpty(downloadUrl)) {
return AjaxResult.error("下载链接不能为空");
}
// 更新Redis中的版本信息和下载链接
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
redisTemplate.opsForValue().set(DOWNLOAD_URL_REDIS_KEY, downloadUrl);
Map<String, Object> result = new HashMap<>();
result.put("version", version);
result.put("downloadUrl", downloadUrl);
result.put("updateTime", System.currentTimeMillis());
return AjaxResult.success("版本信息更新成功", result);
} catch (Exception e) {
return AjaxResult.error("版本信息更新失败: " + e.getMessage());

View File

@@ -0,0 +1,68 @@
package com.ruoyi.web.controller.tool;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.domain.BanmaAccount;
import com.ruoyi.system.service.IBanmaAccountService;
/**
* 斑马账号管理(数据库版,极简接口):
* - 仅负责账号与 Token 的存取
* - 不参与登录/刷新与数据采集,客户端自行处理
*/
@RestController
@RequestMapping("/tool/banma")
@Anonymous
public class BanmaOrderController extends BaseController {
@Autowired
private IBanmaAccountService accountService;
/**
* 查询账号列表(
*/
@GetMapping("/accounts")
public R<?> listAccounts() {
List<BanmaAccount> list = accountService.listSimple();
return R.ok(list);
}
/**
* 新增或编辑账号(含设为默认)
*/
@PostMapping("/accounts")
public R<?> saveAccount(@RequestBody BanmaAccount body) {
Long id = accountService.saveOrUpdate(body);
boolean ok = false;
try { ok = accountService.refreshToken(id); } catch (Exception ignore) {}
return ok ? R.ok(Map.of("id", id)) : R.fail("账号或密码错误无法获取Token");
}
/**
* 删除账号
*/
@DeleteMapping("/accounts/{id}")
public R<?> remove(@PathVariable Long id) {
accountService.remove(id);
return R.ok();
}
/** 手动刷新单个账号 Token */
@PostMapping("/accounts/{id}/refresh-token")
public R<?> refreshOne(@PathVariable Long id) {
accountService.refreshToken(id);
return R.ok();
}
/** 手动刷新全部启用账号 Token */
@PostMapping("/refresh-all")
public R<?> refreshAll() {
accountService.refreshAllTokens();
return R.ok();
}
}

View File

@@ -42,9 +42,6 @@ public class FileController {
@PostMapping("/uploads")
public AjaxResult uploadFiles(@RequestParam("files") List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
return AjaxResult.error("没有选择文件");
}
List<FileDto> fileDtoS = new ArrayList<>();
for (MultipartFile file : files) {

View File

@@ -6,7 +6,6 @@
<el-button type="success" icon="el-icon-upload" size="mini" @click="handleUpload" v-hasPermi="['system:version:upload']">上传新版本</el-button>
</el-form-item>
</el-form>
<!-- 版本信息卡片 -->
<el-row class="mb8">
<el-col s>
@@ -44,14 +43,14 @@
ref="upload"
action="#"
:limit="1"
accept=".exe"
accept=".asar"
:on-exceed="handleExceed"
:file-list="fileList"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove">
<el-button slot="trigger" size="small" type="primary">选择文件</el-button>
<div slot="tip" class="el-upload__tip">只能上传exe文件且不超过800MB</div>
<div slot="tip" class="el-upload__tip">只能上传asar文件且不超过800MB</div>
</el-upload>
</el-form-item>
</el-form>
@@ -169,8 +168,8 @@ export default {
return;
}
if (!this.uploadForm.file.name.endsWith('.exe')) {
this.$modal.msgError("只支持上传.exe文件");
if (!this.uploadForm.file.name.endsWith('.asar')) {
this.$modal.msgError("只支持上传.asar文件");
return;
}