1
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
// package com.tashow.erp.config;
|
||||
//
|
||||
// 已移除 FxWeaver 相关配置(项目改为纯 Spring Boot)。
|
||||
// 如需恢复 JavaFX 集成,请取消注释并恢复依赖。
|
||||
//
|
||||
// import net.rgielen.fxweaver.core.FxWeaver;
|
||||
// import net.rgielen.fxweaver.spring.SpringFxWeaver;
|
||||
// import org.springframework.context.ConfigurableApplicationContext;
|
||||
// import org.springframework.context.annotation.Bean;
|
||||
// import org.springframework.context.annotation.Configuration;
|
||||
//
|
||||
// @Configuration
|
||||
// public class FxWeaverConfig {
|
||||
// @Bean
|
||||
// public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) {
|
||||
// return new SpringFxWeaver(applicationContext);
|
||||
// }
|
||||
// }
|
||||
@@ -1,63 +0,0 @@
|
||||
// package com.tashow.erp.config;
|
||||
//
|
||||
// 已转为纯 Spring Boot,JavaFX 启动屏幕不再使用。如需恢复,可取消注释并补充 JavaFX 依赖。
|
||||
//
|
||||
// import javafx.application.Platform;
|
||||
// import javafx.scene.Scene;
|
||||
// import javafx.scene.control.ProgressBar;
|
||||
// import javafx.scene.image.Image;
|
||||
// import javafx.scene.image.ImageView;
|
||||
// import javafx.scene.layout.StackPane;
|
||||
// import javafx.scene.layout.VBox;
|
||||
// import javafx.stage.Stage;
|
||||
// import javafx.stage.StageStyle;
|
||||
// import lombok.extern.slf4j.Slf4j;
|
||||
// import org.springframework.stereotype.Component;
|
||||
//
|
||||
// /**
|
||||
// * 自定义启动屏幕
|
||||
// */
|
||||
// @Slf4j
|
||||
// @Component
|
||||
// public class MySplashScreen {
|
||||
// private static final String DEFAULT_IMAGE = "/static/image/splash_screen.png";
|
||||
// private Stage splashStage;
|
||||
// private ProgressBar progressBar;
|
||||
//
|
||||
// public void show() {
|
||||
// Platform.runLater(() -> {
|
||||
// try {
|
||||
// splashStage = new Stage();
|
||||
// splashStage.initStyle(StageStyle.UNDECORATED);
|
||||
// Image splashImage = new Image(getClass().getResourceAsStream(DEFAULT_IMAGE));
|
||||
// ImageView imageView = new ImageView(splashImage);
|
||||
// ProgressBar progressBar = new ProgressBar();
|
||||
// progressBar.setPrefWidth(splashImage.getWidth());
|
||||
// progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
// progressBar.setStyle("-fx-accent: #0078d4; -fx-background-color: transparent;");
|
||||
// StackPane root = new StackPane();
|
||||
// VBox progressContainer = new VBox();
|
||||
// progressContainer.setAlignment(javafx.geometry.Pos.BOTTOM_CENTER);
|
||||
// progressContainer.setSpacing(0);
|
||||
// progressContainer.getChildren().add(progressBar);
|
||||
// root.getChildren().addAll(imageView, progressContainer);
|
||||
// Scene scene = new Scene(root);
|
||||
// splashStage.setScene(scene);
|
||||
// splashStage.setResizable(false);
|
||||
// splashStage.centerOnScreen();
|
||||
// splashStage.show();
|
||||
// } catch (Exception e) {
|
||||
// log.error("显示启动屏幕失败", e);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// public void hide() {
|
||||
// Platform.runLater(() -> {
|
||||
// if (splashStage != null) {
|
||||
// splashStage.hide();
|
||||
// log.info("启动屏幕已隐藏");
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
// }
|
||||
// }
|
||||
@@ -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 {
|
||||
// }
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -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(); // 统一的批次时间
|
||||
|
||||
@@ -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);
|
||||
@@ -146,4 +146,70 @@ 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) {
|
||||
Site baseSite = 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 Edg/127.0.0.0")
|
||||
.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||
.addHeader("accept-encoding", "gzip, deflate, br, zstd")
|
||||
.addHeader("cache-control", "max-age=0")
|
||||
.addHeader("upgrade-insecure-requests", "1")
|
||||
.addHeader("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"127\", \"Chromium\";v=\"127\"")
|
||||
.addHeader("sec-ch-ua-mobile", "?0")
|
||||
.addHeader("sec-ch-ua-platform", "\"Windows\"")
|
||||
.addHeader("sec-ch-ua-platform-version", "\"10.0.0\"")
|
||||
.addHeader("sec-ch-device-memory", "8")
|
||||
.addHeader("sec-ch-viewport-width", "1400")
|
||||
.addHeader("device-memory", "8")
|
||||
.addHeader("viewport-width", "1400")
|
||||
.addHeader("dpr", "0.9")
|
||||
.addHeader("downlink", "10")
|
||||
.addHeader("ect", "4g")
|
||||
.addHeader("rtt", "150")
|
||||
.addHeader("sec-fetch-site", "none")
|
||||
.addHeader("sec-fetch-mode", "navigate")
|
||||
.addHeader("sec-fetch-user", "?1")
|
||||
.addHeader("sec-fetch-dest", "document");
|
||||
|
||||
if ("US".equals(region)) {
|
||||
// 美区配置
|
||||
baseSite.addHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
.addHeader("referer", "https://www.amazon.com/")
|
||||
.addCookie("i18n-prefs", "USD")
|
||||
.addCookie("lc-main", "en_US")
|
||||
.addCookie("session-id", "134-6097934-2082600")
|
||||
.addCookie("session-id-time", "2082787201l")
|
||||
.addCookie("ubid-main", "132-7547587-3056927")
|
||||
.addCookie("skin", "noskin")
|
||||
.addCookie("csm-hit", "tb:s-6ZB8JV440R1VZ54PSE5W|1759200029304&t:1759200030204&adb:adblk_no");
|
||||
} else {
|
||||
// 日区配置
|
||||
baseSite.addHeader("accept-language", "ja,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
|
||||
.addHeader("referer", "https://www.amazon.co.jp/")
|
||||
.addCookie("i18n-prefs", "JPY")
|
||||
.addCookie("lc-acbjp", "zh_CN")
|
||||
.addCookie("session-id", "358-1261309-0483141")
|
||||
.addCookie("session-id-time", "2082787201l")
|
||||
.addCookie("ubid-acbjp", "357-8224002-9668932")
|
||||
.addCookie("skin", "noskin");
|
||||
}
|
||||
|
||||
return baseSite;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.root {
|
||||
/*-fx-background-color: #f0f0f0;*/
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 |
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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 };
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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;
|
||||
})();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
Binary file not shown.
Binary file not shown.
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
Reference in New Issue
Block a user