环境搭建配置
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,6 +3,7 @@ frontend-vue/node_modules/
|
||||
|
||||
# frontend-vue build output
|
||||
frontend-vue/dist/
|
||||
frontend-vue/new_web_source
|
||||
|
||||
# generated type declarations
|
||||
frontend-vue/auto-imports.d.ts
|
||||
@@ -28,3 +29,6 @@ backend-java/src/main/resources/application-local.yml
|
||||
# backend-java IDE files
|
||||
backend-java/.idea/
|
||||
backend-java/*.iml
|
||||
|
||||
# desktop app
|
||||
desktop/
|
||||
|
||||
12
app/.env
Normal file
12
app/.env
Normal file
@@ -0,0 +1,12 @@
|
||||
base_url=http://159.75.121.33:15124
|
||||
workflow_id=7608812635877900322
|
||||
mysql_host=159.75.121.33
|
||||
mysql_user=AIimage
|
||||
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=r4m8dov52giup2o
|
||||
proxy_mode=2
|
||||
|
||||
client_name=NanriAI
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
|
||||
|
||||
BIN
app/__pycache__/app.cpython-312.pyc
Normal file
BIN
app/__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/app_common.cpython-312.pyc
Normal file
BIN
app/__pycache__/app_common.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/html_crypto.cpython-312.pyc
Normal file
BIN
app/__pycache__/html_crypto.cpython-312.pyc
Normal file
Binary file not shown.
@@ -37,7 +37,7 @@ app = create_app()
|
||||
|
||||
|
||||
def run_app(host='127.0.0.1', port=5123):
|
||||
init_db()
|
||||
# init_db()
|
||||
app.run(host=host, port=port, threaded=True, use_reloader=False)
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from config import mysql_host, mysql_user, mysql_password, mysql_database
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
STATIC_DIR = os.path.join(BASE_DIR, 'static')
|
||||
ASSETS_DIR = os.path.join(BASE_DIR, 'assets')
|
||||
|
||||
|
||||
def get_db():
|
||||
@@ -42,6 +43,22 @@ def _render_html(template_name: str, **context):
|
||||
return render_template_string(content, **context)
|
||||
|
||||
|
||||
def _render_html_new(template_name: str, **context):
|
||||
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
|
||||
path = os.path.join(BASE_DIR, "web_source", template_name)
|
||||
if not os.path.isfile(path):
|
||||
return render_template(template_name, **context)
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
try:
|
||||
from html_crypto import decrypt
|
||||
content = decrypt(raw).decode("utf-8")
|
||||
except Exception:
|
||||
content = raw.decode("utf-8", errors="replace")
|
||||
return render_template_string(content, **context)
|
||||
|
||||
|
||||
|
||||
def _is_session_user_valid():
|
||||
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
|
||||
uid = session.get('user_id')
|
||||
|
||||
1
app/assets/convert-D4Yv2oRy.css
Normal file
1
app/assets/convert-D4Yv2oRy.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert.js
Normal file
1
app/assets/convert.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-BHE_BCAJ.css
Normal file
1
app/assets/dedupe-BHE_BCAJ.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe.js
Normal file
1
app/assets/dedupe.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/pywebview-Chz98E9c.css
Normal file
1
app/assets/pywebview-Chz98E9c.css
Normal file
File diff suppressed because one or more lines are too long
55
app/assets/pywebview-Rm5Hhqpf.js
Normal file
55
app/assets/pywebview-Rm5Hhqpf.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/split-_O6NHqDj.css
Normal file
1
app/assets/split-_O6NHqDj.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/split.js
Normal file
1
app/assets/split.js
Normal file
File diff suppressed because one or more lines are too long
BIN
app/blueprints/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/admin.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/auth.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/brand.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/brand.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/image.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/image.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
@@ -2,7 +2,8 @@
|
||||
主页面蓝图:首页、home、图片工作台、品牌页、静态文件、Logo
|
||||
"""
|
||||
import os
|
||||
from flask import Blueprint, send_file
|
||||
import requests
|
||||
from flask import Blueprint, Response, request, send_file
|
||||
|
||||
from app_common import (
|
||||
get_db,
|
||||
@@ -16,7 +17,9 @@ from app_common import (
|
||||
)
|
||||
from flask import redirect, url_for, session
|
||||
|
||||
from config import base_url,version
|
||||
from config import base_url,version,java_api_base
|
||||
|
||||
JAVA_API_BASE = java_api_base
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@@ -90,6 +93,36 @@ def serve_new_web_source(filename):
|
||||
|
||||
|
||||
|
||||
@main_bp.route('/newApi/<path:subpath>', methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
|
||||
def proxy_new_api(subpath):
|
||||
target_url = f"{JAVA_API_BASE}/{subpath}"
|
||||
try:
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in request.headers.items()
|
||||
if key.lower() not in {'host', 'content-length'}
|
||||
}
|
||||
upstream = requests.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
params=request.args,
|
||||
data=request.get_data(),
|
||||
headers=headers,
|
||||
cookies=request.cookies,
|
||||
allow_redirects=False,
|
||||
timeout=300,
|
||||
)
|
||||
excluded_headers = {'content-encoding', 'content-length', 'transfer-encoding', 'connection'}
|
||||
response_headers = [
|
||||
(name, value)
|
||||
for name, value in upstream.headers.items()
|
||||
if name.lower() not in excluded_headers
|
||||
]
|
||||
return Response(upstream.content, upstream.status_code, response_headers)
|
||||
except requests.RequestException as exc:
|
||||
return {'success': False, 'message': str(exc), 'data': None}, 502
|
||||
|
||||
|
||||
@main_bp.route('/logo.jpg', methods=['GET'])
|
||||
def get_logo_image():
|
||||
return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg')
|
||||
|
||||
BIN
app/brand_spider/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/brand_spider/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/brand_spider/__pycache__/web_dec.cpython-312.pyc
Normal file
BIN
app/brand_spider/__pycache__/web_dec.cpython-312.pyc
Normal file
Binary file not shown.
@@ -7,6 +7,7 @@ import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
||||
java_api_base = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
||||
|
||||
# MySQL 配置
|
||||
mysql_host = os.getenv("mysql_host")
|
||||
@@ -16,6 +17,7 @@ mysql_database = os.getenv("mysql_database","aiimage")
|
||||
proxy_url = os.getenv("proxy_url")
|
||||
proxy_mode = int(os.getenv("proxy_mode",1))
|
||||
|
||||
|
||||
client_name=os.getenv("client_name") + ".exe"
|
||||
|
||||
cache_path = "./user_data"
|
||||
|
||||
28
app/main.py
28
app/main.py
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
基于 pywebview 实现的跨平台 UI,需先登录方可使用
|
||||
"""
|
||||
from config import cache_path,debug,version
|
||||
from config import cache_path,debug,version,java_api_base
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
@@ -15,6 +15,7 @@ if not debug:
|
||||
import webview
|
||||
import threading
|
||||
import time
|
||||
import requests
|
||||
|
||||
|
||||
with open("version.txt","w",encoding="utf-8") as file:
|
||||
@@ -25,6 +26,7 @@ with open("version.txt","w",encoding="utf-8") as file:
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
APP_URL = "http://127.0.0.1:5123"
|
||||
PORT = 5123
|
||||
JAVA_API_BASE = java_api_base
|
||||
|
||||
|
||||
def generate_images(params):
|
||||
@@ -143,6 +145,28 @@ class WindowAPI:
|
||||
)
|
||||
return result[0] if result else ''
|
||||
|
||||
def upload_file_to_java(self, file_path):
|
||||
"""按本地路径读取文件并上传到 Java 后端临时目录。"""
|
||||
if not file_path or not str(file_path).strip():
|
||||
return {'success': False, 'error': '文件路径为空'}
|
||||
normalized_path = os.path.abspath(str(file_path).strip())
|
||||
if not os.path.isfile(normalized_path):
|
||||
return {'success': False, 'error': '文件不存在'}
|
||||
try:
|
||||
with open(normalized_path, 'rb') as file_obj:
|
||||
response = requests.post(
|
||||
f"{JAVA_API_BASE}/api/files/upload",
|
||||
files={'file': (os.path.basename(normalized_path), file_obj)},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not isinstance(payload, dict):
|
||||
return {'success': False, 'error': 'Java 返回格式错误'}
|
||||
return payload
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def save_file_from_url(self, url, default_filename='download.zip'):
|
||||
"""弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。"""
|
||||
if not url or not url.strip():
|
||||
@@ -332,7 +356,7 @@ def main():
|
||||
easy_drag=True
|
||||
)
|
||||
api = WindowAPI(window)
|
||||
window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip)
|
||||
window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.upload_file_to_java, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip)
|
||||
webview.start(
|
||||
debug=True,
|
||||
storage_path=cache_path,
|
||||
|
||||
15
app/new_web_source/convert.html
Normal file
15
app/new_web_source/convert.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Rm5Hhqpf.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Chz98E9c.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-D4Yv2oRy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
app/new_web_source/dedupe.html
Normal file
15
app/new_web_source/dedupe.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Rm5Hhqpf.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Chz98E9c.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-BHE_BCAJ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
app/new_web_source/split.html
Normal file
15
app/new_web_source/split.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Rm5Hhqpf.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Chz98E9c.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-_O6NHqDj.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
app/tool/__pycache__/devices.cpython-312.pyc
Normal file
BIN
app/tool/__pycache__/devices.cpython-312.pyc
Normal file
Binary file not shown.
1
app/version.txt
Normal file
1
app/version.txt
Normal file
@@ -0,0 +1 @@
|
||||
{"version": "1.0.3"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,11 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
@@ -12,9 +17,25 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.cors(Customizer.withDefaults())
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(List.of("*"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setExposedHeaders(List.of("Content-Disposition"));
|
||||
configuration.setAllowCredentials(false);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.nanri.aiimage.modules.file.controller;
|
||||
|
||||
import com.nanri.aiimage.common.api.ApiResponse;
|
||||
import com.nanri.aiimage.modules.file.model.vo.ExcelInfoVo;
|
||||
import com.nanri.aiimage.modules.file.model.vo.UploadFileVo;
|
||||
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -11,8 +12,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -46,4 +49,14 @@ public class FileUploadController {
|
||||
MultipartFile file) throws Exception {
|
||||
return ApiResponse.success(localFileStorageService.saveTempFile(file));
|
||||
}
|
||||
|
||||
@GetMapping("/excel-info")
|
||||
@Operation(summary = "读取 Excel 信息", description = "根据 fileKey 读取 Excel 第一行表头及总行数,供前端选择保留列和展示总条数。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "读取成功", content = @Content(schema = @Schema(implementation = ExcelInfoVo.class))),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "源文件不存在")
|
||||
})
|
||||
public ApiResponse<ExcelInfoVo> excelInfo(@RequestParam String fileKey) throws Exception {
|
||||
return ApiResponse.success(localFileStorageService.getExcelInfo(fileKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.nanri.aiimage.modules.file.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "Excel 文件信息")
|
||||
public class ExcelInfoVo {
|
||||
|
||||
@Schema(description = "表头列表")
|
||||
private List<String> headers;
|
||||
|
||||
@Schema(description = "数据总行数")
|
||||
private Integer totalRows;
|
||||
}
|
||||
@@ -2,14 +2,26 @@ package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.config.StorageProperties;
|
||||
import com.nanri.aiimage.modules.file.model.vo.ExcelInfoVo;
|
||||
import com.nanri.aiimage.modules.file.model.vo.UploadFileVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -36,4 +48,73 @@ public class LocalFileStorageService {
|
||||
vo.setSize(file.getSize());
|
||||
return vo;
|
||||
}
|
||||
|
||||
public ExcelInfoVo getExcelInfo(String fileKey) throws IOException {
|
||||
File inputFile = findLocalSourceFile(fileKey);
|
||||
if (inputFile == null || !inputFile.exists()) {
|
||||
throw new BusinessException("源文件不存在");
|
||||
}
|
||||
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
Row headerRow = sheet.getRow(0);
|
||||
if (headerRow == null) {
|
||||
throw new BusinessException("Excel 表头为空");
|
||||
}
|
||||
|
||||
Map<String, Integer> headerMap = new HashMap<>();
|
||||
List<String> headers = new ArrayList<>();
|
||||
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
|
||||
Cell cell = headerRow.getCell(i);
|
||||
String value = normalizeCellText(cell == null ? null : formatter.formatCellValue(cell));
|
||||
if (value.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
value = value.split("idASIN国家状态价格变体数量", 2)[0].trim().isEmpty()
|
||||
? value
|
||||
: value.split("idASIN国家状态价格变体数量", 2)[0].trim();
|
||||
if (headerMap.containsKey(value)) {
|
||||
continue;
|
||||
}
|
||||
headerMap.put(value, i);
|
||||
headers.add(value);
|
||||
if ("缩略图地址8".equals(value)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ExcelInfoVo vo = new ExcelInfoVo();
|
||||
vo.setHeaders(headers);
|
||||
vo.setTotalRows(Math.max(sheet.getLastRowNum(), 0));
|
||||
return vo;
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("读取 Excel 信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
private File findLocalSourceFile(String fileKey) {
|
||||
File baseDir = FileUtil.file(storageProperties.getLocalTempDir());
|
||||
if (!baseDir.exists()) {
|
||||
return null;
|
||||
}
|
||||
List<File> matchedFiles = FileUtil.loopFiles(baseDir, pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey));
|
||||
return matchedFiles.isEmpty() ? null : matchedFiles.getFirst();
|
||||
}
|
||||
|
||||
private String normalizeCellText(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.replace("\ufeff", "")
|
||||
.replace("\u3000", " ")
|
||||
.replace("\r\n", " ")
|
||||
.replace("\r", " ")
|
||||
.replace("\n", " ")
|
||||
.replace("\t", " ")
|
||||
.trim()
|
||||
.replaceAll("\\s+", " ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<template>
|
||||
<div class="page-shell module-page">
|
||||
<BrandTopBar active="convert" />
|
||||
|
||||
<div class="main-content">
|
||||
<aside class="left-panel">
|
||||
<div class="section-title">选择文件</div>
|
||||
@@ -9,14 +12,6 @@
|
||||
<button type="button" class="opt-btn" @click="selectConvertFolder">选择文件夹</button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="!hasPywebviewSupport"
|
||||
class="desktop-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="当前环境未检测到 pywebview,文件选择与本地格式转换能力需要在桌面端使用。"
|
||||
/>
|
||||
|
||||
<div class="selected-files clean-placeholder">
|
||||
<template v-if="convertSelectedPaths.length">
|
||||
<span v-for="path in convertDisplayPaths" :key="path">{{ path }}</span>
|
||||
@@ -29,10 +24,6 @@
|
||||
</div>
|
||||
|
||||
<div class="section-title">模板选择</div>
|
||||
<div class="template-toolbar">
|
||||
<input v-model="convertTemplateName" class="template-name-input" type="text" placeholder="输入模板自定义名称(可选)" />
|
||||
<button type="button" class="opt-btn small" @click="importConvertTemplate">上传模板</button>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label
|
||||
v-for="template in convertTemplates"
|
||||
@@ -44,28 +35,15 @@
|
||||
<div class="template-item-content">
|
||||
<div>
|
||||
<span class="label">
|
||||
{{ template.label }}
|
||||
<span v-if="template.is_default" class="template-tag">默认</span>
|
||||
<span v-else-if="template.built_in" class="template-tag muted">内置</span>
|
||||
{{ template.templateName }}
|
||||
<span v-if="template.isDefault" class="template-tag">默认</span>
|
||||
<span v-else-if="template.builtIn" class="template-tag muted">内置</span>
|
||||
</span>
|
||||
<div class="desc">输出文件:{{ template.output_filename }}</div>
|
||||
<div class="desc">输出文件:{{ template.outputFilename }}</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="!template.built_in"
|
||||
type="button"
|
||||
class="template-delete-btn"
|
||||
@click.stop.prevent="deleteConvertTemplate(template.id)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="run-row compact-row">
|
||||
<button type="button" class="opt-btn small" :disabled="!convertTemplateId" @click="setConvertDefaultTemplate">
|
||||
设为默认模板
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="section-title">转换说明</div>
|
||||
<div class="option-group">
|
||||
@@ -73,7 +51,7 @@
|
||||
<input type="checkbox" checked disabled />
|
||||
<div>
|
||||
<span class="label">按当前模板输出</span>
|
||||
<div class="desc">当前模板:{{ currentConvertTemplate?.label || '未加载模板' }}</div>
|
||||
<div class="desc">当前模板:{{ currentConvertTemplate?.templateName || '未加载模板' }}</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -107,11 +85,11 @@
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">成功结果</span>
|
||||
<strong>{{ convertSummary.success_count }}</strong>
|
||||
<strong>{{ convertSummary.successCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">失败文件</span>
|
||||
<strong>{{ convertSummary.failed_count }}</strong>
|
||||
<strong>{{ convertSummary.failedCount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,12 +105,12 @@
|
||||
<ul v-else class="task-list clean-result-list">
|
||||
<li
|
||||
v-for="item in convertResultItems"
|
||||
:key="`${item.output_path || item.source_path || item.error || 'convert'}`"
|
||||
:key="`${item.resultId || item.outputFilename || item.sourceFilename}`"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="left">
|
||||
<span class="id" :title="item.source_path">{{ item.source_path || '-' }}</span>
|
||||
<div v-if="item.output_path" class="files">结果下载地址已生成</div>
|
||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||
<div v-if="item.outputFilename" class="files">输出文件:{{ item.outputFilename }}</div>
|
||||
<div v-else-if="item.error" class="files">错误信息:{{ item.error }}</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +119,7 @@
|
||||
{{ item.success ? '已完成' : '失败' }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.success && item.output_path"
|
||||
v-if="item.success && item.downloadUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="downloadConvertResult(item)"
|
||||
@@ -149,10 +127,10 @@
|
||||
下载文件
|
||||
</button>
|
||||
<button
|
||||
v-if="item.result_id"
|
||||
v-if="item.resultId"
|
||||
type="button"
|
||||
class="btn-delete"
|
||||
@click="deleteConvertHistory(item.result_id)"
|
||||
@click="deleteConvertHistoryRecord(item.resultId)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
@@ -163,42 +141,49 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import BrandTopBar from './BrandTopBar.vue'
|
||||
import { expandBrandFolder } from '@/shared/api/brand'
|
||||
import { type ConvertTxtResultItem, type ConvertTxtTemplateItem, getPywebviewApi, hasPywebview } from '@/shared/bridges/pywebview'
|
||||
import { deleteConvertHistory, getConvertHistory, getConvertTemplates, runConvert, type ConvertResultItem, type ConvertRunVo, type ConvertTemplateVo } from '@/shared/api/java-modules'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
|
||||
const hasPywebviewSupport = hasPywebview()
|
||||
const convertSelectedPaths = ref<string[]>([])
|
||||
const convertUploadedFiles = ref<UploadedJavaFile[]>([])
|
||||
const convertRunning = ref(false)
|
||||
const convertOutputDir = ref('')
|
||||
const convertResultItems = ref<ConvertTxtResultItem[]>([])
|
||||
const convertSummary = ref({ total: 0, success_count: 0, failed_count: 0 })
|
||||
const convertTemplates = ref<ConvertTxtTemplateItem[]>([])
|
||||
const convertTemplateId = ref('uk_offer')
|
||||
const convertTemplateName = ref('')
|
||||
const convertResultItems = ref<ConvertResultItem[]>([])
|
||||
const convertSummary = ref<ConvertRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||
const convertTemplates = ref<ConvertTemplateVo[]>([])
|
||||
const convertTemplateId = ref('')
|
||||
const convertDisplayPaths = computed(() => convertSelectedPaths.value.slice(0, 8))
|
||||
const currentConvertTemplate = computed(
|
||||
() => convertTemplates.value.find((item) => item.id === convertTemplateId.value) || null,
|
||||
)
|
||||
|
||||
function shorten(value: string, maxLength: number) {
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
|
||||
}
|
||||
|
||||
function resetConvertResults() {
|
||||
convertSummary.value = { total: 0, success_count: 0, failed_count: 0 }
|
||||
convertOutputDir.value = ''
|
||||
async function uploadPathsToJava(paths: string[]) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.upload_file_to_java) {
|
||||
throw new Error('当前桌面端未提供文件上传桥接能力')
|
||||
}
|
||||
const uploaded: UploadedJavaFile[] = []
|
||||
for (const path of paths) {
|
||||
const result = await api.upload_file_to_java(path)
|
||||
if (!result?.success || !result.data) {
|
||||
throw new Error(result?.error || result?.message || `上传失败:${path}`)
|
||||
}
|
||||
uploaded.push(result.data)
|
||||
}
|
||||
return uploaded
|
||||
}
|
||||
|
||||
async function loadConvertTemplates() {
|
||||
const api = getPywebviewApi()
|
||||
const templates = await api?.list_txt_templates?.()
|
||||
convertTemplates.value = Array.isArray(templates) ? templates : []
|
||||
const defaultTemplate = convertTemplates.value.find((item) => item.is_default)
|
||||
const templates = await getConvertTemplates()
|
||||
convertTemplates.value = templates || []
|
||||
const defaultTemplate = convertTemplates.value.find((item) => item.isDefault)
|
||||
if (defaultTemplate) {
|
||||
convertTemplateId.value = defaultTemplate.id
|
||||
return
|
||||
@@ -208,79 +193,23 @@ async function loadConvertTemplates() {
|
||||
}
|
||||
}
|
||||
|
||||
async function importConvertTemplate() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.import_txt_template) {
|
||||
ElMessage.warning('当前环境不支持模板上传,请在桌面端打开')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await api.import_txt_template(convertTemplateName.value.trim() || undefined)
|
||||
if (!result.success) {
|
||||
if (result.error && result.error !== '用户取消') {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
convertTemplateName.value = ''
|
||||
await loadConvertTemplates()
|
||||
if (result.template?.id) {
|
||||
convertTemplateId.value = result.template.id
|
||||
}
|
||||
ElMessage.success('模板导入成功')
|
||||
}
|
||||
|
||||
async function deleteConvertTemplate(templateId: string) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.delete_txt_template) {
|
||||
ElMessage.warning('当前环境不支持删除模板')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await api.delete_txt_template(templateId)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '删除模板失败')
|
||||
return
|
||||
}
|
||||
|
||||
if (convertTemplateId.value === templateId) {
|
||||
convertTemplateId.value = ''
|
||||
}
|
||||
await loadConvertTemplates()
|
||||
ElMessage.success('模板已删除')
|
||||
}
|
||||
|
||||
async function setConvertDefaultTemplate() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.set_default_txt_template || !convertTemplateId.value) {
|
||||
ElMessage.warning('当前环境不支持设置默认模板')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await api.set_default_txt_template(convertTemplateId.value)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '设置默认模板失败')
|
||||
return
|
||||
}
|
||||
|
||||
await loadConvertTemplates()
|
||||
ElMessage.success('默认模板已更新')
|
||||
async function handleSelectedPaths(paths: string[], successMessage: string) {
|
||||
convertSelectedPaths.value = paths
|
||||
convertUploadedFiles.value = await uploadPathsToJava(paths)
|
||||
ElMessage.success(successMessage)
|
||||
}
|
||||
|
||||
async function selectConvertFiles() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.select_clean_xlsx_files) {
|
||||
ElMessage.warning('当前环境不支持文件选择,请在桌面端打开')
|
||||
if (!api?.select_brand_xlsx_files) {
|
||||
ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const paths = await api.select_clean_xlsx_files()
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
convertSelectedPaths.value = paths
|
||||
resetConvertResults()
|
||||
ElMessage.success(`已选择 ${paths.length} 个待转换文件`)
|
||||
await handleSelectedPaths(paths, `已选择 ${paths.length} 个待转换文件`)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
@@ -288,20 +217,18 @@ async function selectConvertFiles() {
|
||||
|
||||
async function selectConvertFolder() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.select_clean_folder) {
|
||||
ElMessage.warning('当前环境不支持文件夹选择,请在桌面端打开')
|
||||
if (!api?.select_brand_folder) {
|
||||
ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const folder = await api.select_clean_folder()
|
||||
const folder = await api.select_brand_folder()
|
||||
if (!folder) return
|
||||
|
||||
const result = await expandBrandFolder(folder)
|
||||
if (result.success && result.paths?.length) {
|
||||
convertSelectedPaths.value = result.paths
|
||||
resetConvertResults()
|
||||
ElMessage.success(`已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
|
||||
await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -312,34 +239,23 @@ async function selectConvertFolder() {
|
||||
}
|
||||
|
||||
async function submitConvertRun() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.convert_excel_to_uk_txt) {
|
||||
ElMessage.warning('当前环境不支持格式转换,请在桌面端打开')
|
||||
if (!convertUploadedFiles.value.length) {
|
||||
ElMessage.warning('请先选择待转换 Excel 文件或文件夹')
|
||||
return
|
||||
}
|
||||
if (!convertSelectedPaths.value.length) {
|
||||
ElMessage.warning('请先选择待转换 Excel 文件或文件夹')
|
||||
if (!convertTemplateId.value) {
|
||||
ElMessage.warning('请先选择模板')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
convertRunning.value = true
|
||||
const result = await api.convert_excel_to_uk_txt({
|
||||
paths: convertSelectedPaths.value,
|
||||
template_id: convertTemplateId.value,
|
||||
const result = await runConvert({
|
||||
files: convertUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
|
||||
templateId: convertTemplateId.value,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '格式转换失败')
|
||||
return
|
||||
}
|
||||
|
||||
convertOutputDir.value = ''
|
||||
convertSummary.value = {
|
||||
total: result.summary?.total || 0,
|
||||
success_count: result.summary?.success_count || 0,
|
||||
failed_count: result.summary?.failed_count || 0,
|
||||
}
|
||||
convertSummary.value = result
|
||||
convertResultItems.value = result.items || []
|
||||
await loadConvertHistory()
|
||||
ElMessage.success('格式转换完成')
|
||||
} catch (error) {
|
||||
@@ -349,36 +265,41 @@ async function submitConvertRun() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteConvertHistory(resultId: number) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.delete_history_item) {
|
||||
ElMessage.warning('当前环境不支持删除历史')
|
||||
return
|
||||
async function loadConvertHistory() {
|
||||
try {
|
||||
const response = await getConvertHistory()
|
||||
convertResultItems.value = response.items || []
|
||||
convertSummary.value = {
|
||||
total: response.items?.length || 0,
|
||||
successCount: response.items?.filter((item) => item.success).length || 0,
|
||||
failedCount: response.items?.filter((item) => !item.success).length || 0,
|
||||
items: response.items || [],
|
||||
}
|
||||
|
||||
const result = await api.delete_history_item('convert', resultId)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '删除失败')
|
||||
return
|
||||
} catch {
|
||||
// ignore history load errors
|
||||
}
|
||||
await loadConvertHistory()
|
||||
ElMessage.success('已删除')
|
||||
}
|
||||
|
||||
async function downloadConvertResult(item: ConvertTxtResultItem) {
|
||||
async function deleteConvertHistoryRecord(resultId: number) {
|
||||
try {
|
||||
await deleteConvertHistory(resultId)
|
||||
await loadConvertHistory()
|
||||
ElMessage.success('已删除')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadConvertResult(item: ConvertResultItem) {
|
||||
const api = getPywebviewApi()
|
||||
if (!item.output_path) {
|
||||
if (!item.downloadUrl) {
|
||||
ElMessage.warning('当前结果没有下载地址')
|
||||
return
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
const yyyy = String(today.getFullYear())
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(today.getDate()).padStart(2, '0')
|
||||
const filename = `${yyyy}${mm}${dd}.txt`
|
||||
const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.txt`
|
||||
if (api?.save_file_from_url) {
|
||||
const result = await api.save_file_from_url(item.output_path, filename)
|
||||
const result = await api.save_file_from_url(item.downloadUrl, filename)
|
||||
if (result.success) {
|
||||
ElMessage.success(`已保存:${result.path || filename}`)
|
||||
} else if (result.error && result.error !== '用户取消') {
|
||||
@@ -387,25 +308,7 @@ async function downloadConvertResult(item: ConvertTxtResultItem) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(item.output_path, '_blank')
|
||||
}
|
||||
|
||||
async function loadConvertHistory() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.get_convert_history) return
|
||||
|
||||
try {
|
||||
const response = await api.get_convert_history()
|
||||
if (!response.success || !response.items) return
|
||||
convertResultItems.value = response.items
|
||||
convertSummary.value = {
|
||||
total: response.items.length,
|
||||
success_count: response.items.filter((item) => item.success).length,
|
||||
failed_count: response.items.filter((item) => !item.success).length,
|
||||
}
|
||||
} catch {
|
||||
// ignore history load errors
|
||||
}
|
||||
window.open(item.downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -415,7 +318,8 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-content { display: flex; height: calc(100vh - 56px); }
|
||||
.module-page { min-height: 100vh; background: #1a1a1a; }
|
||||
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
|
||||
.left-panel { width: 380px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
||||
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
||||
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||
@@ -423,29 +327,22 @@ onMounted(() => {
|
||||
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
||||
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
|
||||
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
||||
.opt-btn, .btn-run, .download { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||
.opt-btn, .btn-run, .download, .btn-delete { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||
.opt-btn { padding: 8px 16px; font-size: 13px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; }
|
||||
.opt-btn.small { padding: 6px 10px; font-size: 12px; }
|
||||
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; }
|
||||
.desktop-alert { margin-top: 14px; text-align: left; }
|
||||
.selected-files { margin-top: 14px; font-size: 12px; color: #888; max-height: 112px; overflow-y: auto; text-align: left; }
|
||||
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
|
||||
.more-line { color: #b8c1cc; }
|
||||
.option-group { margin-bottom: 20px; }
|
||||
.template-toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.template-name-input { flex: 1; min-width: 0; padding: 8px 12px; border-radius: 8px; border: 1px solid #3a3a3a; background: #202020; color: #e0e0e0; font-size: 13px; }
|
||||
.template-name-input::placeholder { color: #7f8790; }
|
||||
.template-tag { margin-left: 8px; padding: 2px 8px; border-radius: 999px; font-size: 11px; color: #8fd0ff; background: rgba(52, 152, 219, 0.14); }
|
||||
.template-tag.muted { color: #b8b8b8; background: rgba(255, 255, 255, 0.08); }
|
||||
.compact-row { margin-top: 4px; }
|
||||
.radio-item { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; padding: 10px 12px; border-radius: 8px; background: #252525; border: 1px solid #2a2a2a; margin-bottom: 8px; transition: all 0.2s ease; }
|
||||
.radio-item { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; padding: 10px 12px; border-radius: 8px; background: #252525; border: 1px solid #2a2a2a; margin-bottom: 8px; transition: all 0.2s ease; }
|
||||
.template-item-content { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; }
|
||||
.template-delete-btn { padding: 6px 10px; border: 1px solid #5c2f2f; border-radius: 6px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; font-size: 12px; cursor: pointer; }
|
||||
.template-delete-btn:hover { background: rgba(231, 76, 60, 0.2); }
|
||||
.radio-item:hover, .radio-item.active { background: #2a2a2a; border-color: #3a3a3a; }
|
||||
.radio-item input { margin-top: 3px; }
|
||||
.label { font-weight: 500; color: #e0e0e0; font-size: 13px; }
|
||||
.desc { font-size: 12px; color: #888; margin-top: 2px; line-height: 1.5; font-weight: 400; }
|
||||
.template-tag { margin-left: 8px; padding: 2px 8px; border-radius: 999px; font-size: 11px; color: #8fd0ff; background: rgba(52, 152, 219, 0.14); }
|
||||
.template-tag.muted { color: #b8b8b8; background: rgba(255, 255, 255, 0.08); }
|
||||
.run-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 12px; margin-top: 16px; }
|
||||
.btn-run { padding: 10px 22px; background: #3498db; color: #fff; border-radius: 8px; font-size: 14px; }
|
||||
.btn-run:hover { background: #2980b9; }
|
||||
@@ -464,6 +361,8 @@ onMounted(() => {
|
||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
||||
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
|
||||
.empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
|
||||
.clean-placeholder { max-height: none; }
|
||||
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; }
|
||||
@@ -472,7 +371,5 @@ onMounted(() => {
|
||||
.summary-label { font-size: 12px; color: #8d8d8d; }
|
||||
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; }
|
||||
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; }
|
||||
.download-all { padding: 7px 12px; border: 1px solid #2f5f85; border-radius: 6px; background: rgba(52, 152, 219, 0.14); color: #69b6ff; font-size: 12px; cursor: pointer; }
|
||||
.download-all:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -1,4 +1,7 @@
|
||||
<template>
|
||||
<div class="page-shell module-page">
|
||||
<BrandTopBar active="dedupe" />
|
||||
|
||||
<div class="main-content">
|
||||
<aside class="left-panel">
|
||||
<div class="section-title">选择文件</div>
|
||||
@@ -9,14 +12,6 @@
|
||||
<button type="button" class="opt-btn" @click="selectCleanFolder">选择文件夹</button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="!hasPywebviewSupport"
|
||||
class="desktop-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="当前环境未检测到 pywebview,文件选择与 Excel 表头读取能力需要在桌面端使用。"
|
||||
/>
|
||||
|
||||
<div class="selected-files clean-placeholder">
|
||||
<template v-if="cleanSelectedPaths.length">
|
||||
<span v-for="path in cleanDisplayPaths" :key="path">{{ path }}</span>
|
||||
@@ -95,11 +90,11 @@
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">成功结果</span>
|
||||
<strong>{{ cleanSummary.success_count }}</strong>
|
||||
<strong>{{ cleanSummary.successCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">失败文件</span>
|
||||
<strong>{{ cleanSummary.failed_count }}</strong>
|
||||
<strong>{{ cleanSummary.failedCount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,10 +108,10 @@
|
||||
</div>
|
||||
|
||||
<ul v-else class="task-list clean-result-list">
|
||||
<li v-for="item in cleanResultItems" :key="`${item.source_path}-${item.output_path || item.error || 'result'}`" class="task-item">
|
||||
<li v-for="item in cleanResultItems" :key="`${item.resultId || item.outputFilename || item.sourceFilename}`" class="task-item">
|
||||
<div class="left">
|
||||
<span class="id" :title="item.source_path">{{ item.source_path || '-' }}</span>
|
||||
<div v-if="item.output_path" class="files">结果下载地址已生成</div>
|
||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||
<div v-if="item.outputFilename" class="files">输出文件:{{ item.outputFilename }}</div>
|
||||
<div v-else-if="item.error" class="files">错误信息:{{ item.error }}</div>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +120,7 @@
|
||||
{{ item.success ? '已完成' : '失败' }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.success && item.output_path"
|
||||
v-if="item.success && item.downloadUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="downloadCleanResult(item)"
|
||||
@@ -133,10 +128,10 @@
|
||||
下载文件
|
||||
</button>
|
||||
<button
|
||||
v-if="item.result_id"
|
||||
v-if="item.resultId"
|
||||
type="button"
|
||||
class="btn-delete"
|
||||
@click="deleteCleanHistory(item.result_id)"
|
||||
@click="deleteCleanHistoryRecord(item.resultId)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
@@ -147,30 +142,28 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import BrandTopBar from './BrandTopBar.vue'
|
||||
import { expandBrandFolder } from '@/shared/api/brand'
|
||||
import { type CleanExcelResultItem, getPywebviewApi, hasPywebview } from '@/shared/bridges/pywebview'
|
||||
import { deleteDedupeHistory, getDedupeHistory, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunVo } from '@/shared/api/java-modules'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
|
||||
const hasPywebviewSupport = hasPywebview()
|
||||
const cleanAvailableColumns = ref<string[]>([])
|
||||
const cleanSelectedColumns = ref<string[]>([])
|
||||
const cleanSelectedPaths = ref<string[]>([])
|
||||
const cleanUploadedFiles = ref<UploadedJavaFile[]>([])
|
||||
const cleanKeepIntegerIds = ref(true)
|
||||
const cleanKeepUnderscoreIds = ref(true)
|
||||
const cleanRunning = ref(false)
|
||||
const cleanOutputDir = ref('')
|
||||
const cleanResultItems = ref<CleanExcelResultItem[]>([])
|
||||
const cleanSummary = ref({ total: 0, success_count: 0, failed_count: 0 })
|
||||
const cleanResultItems = ref<DedupeResultItem[]>([])
|
||||
const cleanSummary = ref<DedupeRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||
const cleanDisplayPaths = computed(() => cleanSelectedPaths.value.slice(0, 8))
|
||||
|
||||
function shorten(value: string, maxLength: number) {
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
|
||||
}
|
||||
|
||||
function selectAllCleanColumns() {
|
||||
cleanSelectedColumns.value = [...cleanAvailableColumns.value]
|
||||
}
|
||||
@@ -179,19 +172,26 @@ function clearAllCleanColumns() {
|
||||
cleanSelectedColumns.value = []
|
||||
}
|
||||
|
||||
async function loadCleanHeaders(filePath: string) {
|
||||
async function uploadPathsToJava(paths: string[]) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.get_excel_headers) {
|
||||
ElMessage.warning('当前环境不支持 Excel 表头读取,请在桌面端打开')
|
||||
return
|
||||
if (!api?.upload_file_to_java) {
|
||||
throw new Error('当前桌面端未提供文件上传桥接能力')
|
||||
}
|
||||
const uploaded: UploadedJavaFile[] = []
|
||||
for (const path of paths) {
|
||||
const result = await api.upload_file_to_java(path)
|
||||
if (!result?.success || !result.data) {
|
||||
throw new Error(result?.error || result?.message || `上传失败:${path}`)
|
||||
}
|
||||
uploaded.push(result.data)
|
||||
}
|
||||
return uploaded
|
||||
}
|
||||
|
||||
cleanSummary.value = { total: 0, success_count: 0, failed_count: 0 }
|
||||
cleanOutputDir.value = ''
|
||||
|
||||
const result = await api.get_excel_headers(filePath)
|
||||
if (!result.success || !result.headers?.length) {
|
||||
ElMessage.error(result.error || '读取 Excel 表头失败')
|
||||
async function loadCleanHeaders(fileKey: string) {
|
||||
const result = await getExcelInfo(fileKey)
|
||||
if (!result.headers?.length) {
|
||||
ElMessage.error('读取 Excel 表头失败')
|
||||
cleanAvailableColumns.value = []
|
||||
cleanSelectedColumns.value = []
|
||||
return
|
||||
@@ -207,19 +207,26 @@ async function loadCleanHeaders(filePath: string) {
|
||||
cleanSelectedColumns.value = preferredHeaders.filter((header) => orderedHeaders.includes(header))
|
||||
}
|
||||
|
||||
async function handleSelectedPaths(paths: string[], successMessage: string) {
|
||||
cleanSelectedPaths.value = paths
|
||||
cleanUploadedFiles.value = await uploadPathsToJava(paths)
|
||||
if (cleanUploadedFiles.value.length > 0) {
|
||||
await loadCleanHeaders(cleanUploadedFiles.value[0].fileKey)
|
||||
}
|
||||
ElMessage.success(successMessage)
|
||||
}
|
||||
|
||||
async function selectCleanFiles() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.select_clean_xlsx_files) {
|
||||
ElMessage.warning('当前环境不支持文件选择,请在桌面端打开')
|
||||
if (!api?.select_brand_xlsx_files) {
|
||||
ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const paths = await api.select_clean_xlsx_files()
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
cleanSelectedPaths.value = paths
|
||||
await loadCleanHeaders(paths[0])
|
||||
ElMessage.success(`已选择 ${paths.length} 个待清洗文件`)
|
||||
await handleSelectedPaths(paths, `已选择 ${paths.length} 个待清洗文件`)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
@@ -227,20 +234,18 @@ async function selectCleanFiles() {
|
||||
|
||||
async function selectCleanFolder() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.select_clean_folder) {
|
||||
ElMessage.warning('当前环境不支持文件夹选择,请在桌面端打开')
|
||||
if (!api?.select_brand_folder) {
|
||||
ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const folder = await api.select_clean_folder()
|
||||
const folder = await api.select_brand_folder()
|
||||
if (!folder) return
|
||||
|
||||
const result = await expandBrandFolder(folder)
|
||||
if (result.success && result.paths?.length) {
|
||||
cleanSelectedPaths.value = result.paths
|
||||
await loadCleanHeaders(result.paths[0])
|
||||
ElMessage.success(`已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
|
||||
await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -251,12 +256,7 @@ async function selectCleanFolder() {
|
||||
}
|
||||
|
||||
async function submitCleanRun() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.clean_excel_files) {
|
||||
ElMessage.warning('当前环境不支持数据去重,请在桌面端打开')
|
||||
return
|
||||
}
|
||||
if (!cleanSelectedPaths.value.length) {
|
||||
if (!cleanUploadedFiles.value.length) {
|
||||
ElMessage.warning('请先选择待清洗 Excel 文件或文件夹')
|
||||
return
|
||||
}
|
||||
@@ -271,24 +271,14 @@ async function submitCleanRun() {
|
||||
|
||||
try {
|
||||
cleanRunning.value = true
|
||||
const result = await api.clean_excel_files({
|
||||
paths: cleanSelectedPaths.value,
|
||||
selected_columns: cleanSelectedColumns.value,
|
||||
keep_integer_ids: cleanKeepIntegerIds.value,
|
||||
keep_underscore_ids: cleanKeepUnderscoreIds.value,
|
||||
const result = await runDedupe({
|
||||
files: cleanUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
|
||||
selectedColumns: cleanSelectedColumns.value,
|
||||
keepIntegerIds: cleanKeepIntegerIds.value,
|
||||
keepUnderscoreIds: cleanKeepUnderscoreIds.value,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '清洗失败')
|
||||
return
|
||||
}
|
||||
|
||||
cleanOutputDir.value = ''
|
||||
cleanSummary.value = {
|
||||
total: result.summary?.total || 0,
|
||||
success_count: result.summary?.success_count || 0,
|
||||
failed_count: result.summary?.failed_count || 0,
|
||||
}
|
||||
cleanSummary.value = result
|
||||
cleanResultItems.value = result.items || []
|
||||
await loadCleanHistory()
|
||||
ElMessage.success('数据去重完成')
|
||||
} catch (error) {
|
||||
@@ -298,60 +288,41 @@ async function submitCleanRun() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCleanHistory().catch(() => undefined)
|
||||
})
|
||||
|
||||
async function loadCleanHistory() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.get_dedupe_history) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get_dedupe_history()
|
||||
if (!response.success || !response.items) return
|
||||
cleanResultItems.value = response.items
|
||||
const response = await getDedupeHistory()
|
||||
cleanResultItems.value = response.items || []
|
||||
cleanSummary.value = {
|
||||
total: response.items.length,
|
||||
success_count: response.items.filter((item) => item.success).length,
|
||||
failed_count: response.items.filter((item) => !item.success).length,
|
||||
total: response.items?.length || 0,
|
||||
successCount: response.items?.filter((item) => item.success).length || 0,
|
||||
failedCount: response.items?.filter((item) => !item.success).length || 0,
|
||||
items: response.items || [],
|
||||
}
|
||||
} catch {
|
||||
// ignore history load errors
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCleanHistory(resultId: number) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.delete_history_item) {
|
||||
ElMessage.warning('当前环境不支持删除历史')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await api.delete_history_item('dedupe', resultId)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '删除失败')
|
||||
return
|
||||
}
|
||||
async function deleteCleanHistoryRecord(resultId: number) {
|
||||
try {
|
||||
await deleteDedupeHistory(resultId)
|
||||
await loadCleanHistory()
|
||||
ElMessage.success('已删除')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadCleanResult(item: CleanExcelResultItem) {
|
||||
async function downloadCleanResult(item: DedupeResultItem) {
|
||||
const api = getPywebviewApi()
|
||||
if (!item.output_path) {
|
||||
if (!item.downloadUrl) {
|
||||
ElMessage.warning('当前结果没有下载地址')
|
||||
return
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
const yyyy = String(today.getFullYear())
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(today.getDate()).padStart(2, '0')
|
||||
const filename = `${yyyy}${mm}${dd}_cleaned.xlsx`
|
||||
const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_cleaned.xlsx`
|
||||
if (api?.save_file_from_url) {
|
||||
const result = await api.save_file_from_url(item.output_path, filename)
|
||||
const result = await api.save_file_from_url(item.downloadUrl, filename)
|
||||
if (result.success) {
|
||||
ElMessage.success(`已保存:${result.path || filename}`)
|
||||
} else if (result.error && result.error !== '用户取消') {
|
||||
@@ -360,12 +331,17 @@ async function downloadCleanResult(item: CleanExcelResultItem) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(item.output_path, '_blank')
|
||||
window.open(item.downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCleanHistory().catch(() => undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-content { display: flex; height: calc(100vh - 56px); }
|
||||
.module-page { min-height: 100vh; background: #1a1a1a; }
|
||||
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
|
||||
.left-panel { width: 380px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
||||
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
||||
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||
@@ -373,11 +349,10 @@ async function downloadCleanResult(item: CleanExcelResultItem) {
|
||||
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
||||
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
|
||||
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
||||
.opt-btn, .btn-run, .download { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||
.opt-btn, .btn-run, .download, .btn-delete { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||
.opt-btn { padding: 8px 16px; font-size: 13px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; }
|
||||
.opt-btn.small { padding: 6px 10px; font-size: 12px; }
|
||||
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; }
|
||||
.desktop-alert { margin-top: 14px; text-align: left; }
|
||||
.selected-files { margin-top: 14px; font-size: 12px; color: #888; max-height: 112px; overflow-y: auto; text-align: left; }
|
||||
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
|
||||
.more-line { color: #b8c1cc; }
|
||||
@@ -415,6 +390,8 @@ async function downloadCleanResult(item: CleanExcelResultItem) {
|
||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
||||
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
|
||||
.empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
|
||||
.clean-placeholder { max-height: none; }
|
||||
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; }
|
||||
@@ -423,7 +400,5 @@ async function downloadCleanResult(item: CleanExcelResultItem) {
|
||||
.summary-label { font-size: 12px; color: #8d8d8d; }
|
||||
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; }
|
||||
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; }
|
||||
.download-all { padding: 7px 12px; border: 1px solid #2f5f85; border-radius: 6px; background: rgba(52, 152, 219, 0.14); color: #69b6ff; font-size: 12px; cursor: pointer; }
|
||||
.download-all:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -1,4 +1,7 @@
|
||||
<template>
|
||||
<div class="page-shell module-page">
|
||||
<BrandTopBar active="split" />
|
||||
|
||||
<div class="main-content">
|
||||
<aside class="left-panel">
|
||||
<div class="section-title">选择文件</div>
|
||||
@@ -105,11 +108,11 @@
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">成功结果</span>
|
||||
<strong>{{ splitSummary.success_count }}</strong>
|
||||
<strong>{{ splitSummary.successCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">失败文件</span>
|
||||
<strong>{{ splitSummary.failed_count }}</strong>
|
||||
<strong>{{ splitSummary.failedCount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -123,11 +126,11 @@
|
||||
</div>
|
||||
|
||||
<ul v-else class="task-list clean-result-list">
|
||||
<li v-for="item in splitResultItems" :key="`${item.output_path || item.source_path}-${item.row_count || 0}`" class="task-item">
|
||||
<li v-for="item in splitResultItems" :key="`${item.resultId || item.outputFilename || item.sourceFilename}-${item.rowCount || 0}`" class="task-item">
|
||||
<div class="left">
|
||||
<span class="id" :title="item.source_path">{{ item.source_path || '-' }}</span>
|
||||
<div v-if="item.output_path" class="files">结果下载地址已生成</div>
|
||||
<div v-if="item.row_count !== undefined" class="time">包含 {{ item.row_count }} 条数据</div>
|
||||
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
|
||||
<div v-if="item.outputFilename" class="files">输出文件:{{ item.outputFilename }}</div>
|
||||
<div v-if="item.rowCount !== undefined" class="time">包含 {{ item.rowCount }} 条数据</div>
|
||||
<div v-else-if="item.error" class="files">错误信息:{{ item.error }}</div>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +139,7 @@
|
||||
{{ item.success ? '已完成' : '失败' }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.success && item.output_path"
|
||||
v-if="item.success && item.downloadUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="downloadSplitResult(item)"
|
||||
@@ -144,10 +147,10 @@
|
||||
下载文件
|
||||
</button>
|
||||
<button
|
||||
v-if="item.result_id"
|
||||
v-if="item.resultId"
|
||||
type="button"
|
||||
class="btn-delete"
|
||||
@click="deleteSplitHistory(item.result_id)"
|
||||
@click="deleteSplitHistoryRecord(item.resultId)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
@@ -158,15 +161,19 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import BrandTopBar from './BrandTopBar.vue'
|
||||
import { expandBrandFolder } from '@/shared/api/brand'
|
||||
import { type SplitExcelResultItem, getPywebviewApi, hasPywebview } from '@/shared/bridges/pywebview'
|
||||
import { deleteSplitHistory, getExcelInfo, getSplitHistory, runSplit, type SplitResultItem, type SplitRunVo } from '@/shared/api/java-modules'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
|
||||
const splitSelectedPaths = ref<string[]>([])
|
||||
const splitUploadedFiles = ref<UploadedJavaFile[]>([])
|
||||
const splitAvailableColumns = ref<string[]>([])
|
||||
const splitSelectedColumns = ref<string[]>([])
|
||||
const splitTotalRows = ref(0)
|
||||
@@ -174,55 +181,53 @@ const splitMode = ref<'rows_per_file' | 'parts'>('rows_per_file')
|
||||
const splitRowsPerFile = ref<number | null>(2000)
|
||||
const splitParts = ref<number | null>(5)
|
||||
const splitRunning = ref(false)
|
||||
const splitOutputDir = ref('')
|
||||
const splitResultItems = ref<SplitExcelResultItem[]>([])
|
||||
const splitSummary = ref({ total: 0, success_count: 0, failed_count: 0 })
|
||||
const splitResultItems = ref<SplitResultItem[]>([])
|
||||
const splitSummary = ref<SplitRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||
const splitDisplayPaths = computed(() => splitSelectedPaths.value.slice(0, 8))
|
||||
|
||||
function shorten(value: string, maxLength: number) {
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
|
||||
}
|
||||
|
||||
function resetSplitResults() {
|
||||
splitSummary.value = { total: 0, success_count: 0, failed_count: 0 }
|
||||
splitOutputDir.value = ''
|
||||
}
|
||||
|
||||
async function loadSplitInfo(filePath: string) {
|
||||
async function uploadPathsToJava(paths: string[]) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.get_excel_info) {
|
||||
ElMessage.warning('当前环境不支持读取 Excel 信息,请在桌面端打开')
|
||||
return
|
||||
if (!api?.upload_file_to_java) {
|
||||
throw new Error('当前桌面端未提供文件上传桥接能力')
|
||||
}
|
||||
|
||||
resetSplitResults()
|
||||
const result = await api.get_excel_info(filePath)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '读取 Excel 信息失败')
|
||||
splitAvailableColumns.value = []
|
||||
splitSelectedColumns.value = []
|
||||
splitTotalRows.value = 0
|
||||
return
|
||||
const uploaded: UploadedJavaFile[] = []
|
||||
for (const path of paths) {
|
||||
const result = await api.upload_file_to_java(path)
|
||||
if (!result?.success || !result.data) {
|
||||
throw new Error(result?.error || result?.message || `上传失败:${path}`)
|
||||
}
|
||||
uploaded.push(result.data)
|
||||
}
|
||||
return uploaded
|
||||
}
|
||||
|
||||
async function loadSplitInfo(fileKey: string) {
|
||||
const result = await getExcelInfo(fileKey)
|
||||
splitAvailableColumns.value = result.headers || []
|
||||
splitSelectedColumns.value = [...(result.headers || [])]
|
||||
splitTotalRows.value = result.total_rows || 0
|
||||
splitTotalRows.value = result.totalRows || 0
|
||||
}
|
||||
|
||||
async function handleSelectedPaths(paths: string[], successMessage: string) {
|
||||
splitSelectedPaths.value = paths
|
||||
splitUploadedFiles.value = await uploadPathsToJava(paths)
|
||||
if (splitUploadedFiles.value.length > 0) {
|
||||
await loadSplitInfo(splitUploadedFiles.value[0].fileKey)
|
||||
}
|
||||
ElMessage.success(successMessage)
|
||||
}
|
||||
|
||||
async function selectSplitFiles() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.select_clean_xlsx_files) {
|
||||
ElMessage.warning('当前环境不支持文件选择,请在桌面端打开')
|
||||
if (!api?.select_brand_xlsx_files) {
|
||||
ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const paths = await api.select_clean_xlsx_files()
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
splitSelectedPaths.value = paths
|
||||
await loadSplitInfo(paths[0])
|
||||
ElMessage.success(`已选择 ${paths.length} 个待拆分文件`)
|
||||
await handleSelectedPaths(paths, `已选择 ${paths.length} 个待拆分文件`)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
@@ -230,20 +235,18 @@ async function selectSplitFiles() {
|
||||
|
||||
async function selectSplitFolder() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.select_clean_folder) {
|
||||
ElMessage.warning('当前环境不支持文件夹选择,请在桌面端打开')
|
||||
if (!api?.select_brand_folder) {
|
||||
ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const folder = await api.select_clean_folder()
|
||||
const folder = await api.select_brand_folder()
|
||||
if (!folder) return
|
||||
|
||||
const result = await expandBrandFolder(folder)
|
||||
if (result.success && result.paths?.length) {
|
||||
splitSelectedPaths.value = result.paths
|
||||
await loadSplitInfo(result.paths[0])
|
||||
ElMessage.success(`已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
|
||||
await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -254,12 +257,7 @@ async function selectSplitFolder() {
|
||||
}
|
||||
|
||||
async function submitSplitRun() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.split_excel_files) {
|
||||
ElMessage.warning('当前环境不支持数据拆分,请在桌面端打开')
|
||||
return
|
||||
}
|
||||
if (!splitSelectedPaths.value.length) {
|
||||
if (!splitUploadedFiles.value.length) {
|
||||
ElMessage.warning('请先选择待拆分 Excel 文件或文件夹')
|
||||
return
|
||||
}
|
||||
@@ -278,25 +276,15 @@ async function submitSplitRun() {
|
||||
|
||||
try {
|
||||
splitRunning.value = true
|
||||
const result = await api.split_excel_files({
|
||||
paths: splitSelectedPaths.value,
|
||||
selected_columns: splitSelectedColumns.value,
|
||||
split_mode: splitMode.value,
|
||||
rows_per_file: splitRowsPerFile.value || undefined,
|
||||
parts: splitParts.value || undefined,
|
||||
const result = await runSplit({
|
||||
files: splitUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
|
||||
selectedColumns: splitSelectedColumns.value,
|
||||
splitMode: splitMode.value,
|
||||
rowsPerFile: splitMode.value === 'rows_per_file' ? splitRowsPerFile.value || undefined : undefined,
|
||||
parts: splitMode.value === 'parts' ? splitParts.value || undefined : undefined,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '数据拆分失败')
|
||||
return
|
||||
}
|
||||
|
||||
splitOutputDir.value = ''
|
||||
splitSummary.value = {
|
||||
total: result.summary?.total || 0,
|
||||
success_count: result.summary?.success_count || 0,
|
||||
failed_count: result.summary?.failed_count || 0,
|
||||
}
|
||||
splitSummary.value = result
|
||||
splitResultItems.value = result.items || []
|
||||
await loadSplitHistory()
|
||||
ElMessage.success('数据拆分完成')
|
||||
} catch (error) {
|
||||
@@ -307,57 +295,40 @@ async function submitSplitRun() {
|
||||
}
|
||||
|
||||
async function loadSplitHistory() {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.get_split_history) return
|
||||
|
||||
try {
|
||||
const response = await api.get_split_history()
|
||||
if (!response.success || !response.items) return
|
||||
splitResultItems.value = response.items
|
||||
const response = await getSplitHistory()
|
||||
splitResultItems.value = response.items || []
|
||||
splitSummary.value = {
|
||||
total: response.items.length,
|
||||
success_count: response.items.filter((item) => item.success).length,
|
||||
failed_count: response.items.filter((item) => !item.success).length,
|
||||
total: response.items?.length || 0,
|
||||
successCount: response.items?.filter((item) => item.success).length || 0,
|
||||
failedCount: response.items?.filter((item) => !item.success).length || 0,
|
||||
items: response.items || [],
|
||||
}
|
||||
} catch {
|
||||
// ignore history load errors
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSplitHistory().catch(() => undefined)
|
||||
})
|
||||
|
||||
async function deleteSplitHistory(resultId: number) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.delete_history_item) {
|
||||
ElMessage.warning('当前环境不支持删除历史')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await api.delete_history_item('split', resultId)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.error || '删除失败')
|
||||
return
|
||||
}
|
||||
async function deleteSplitHistoryRecord(resultId: number) {
|
||||
try {
|
||||
await deleteSplitHistory(resultId)
|
||||
await loadSplitHistory()
|
||||
ElMessage.success('已删除')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSplitResult(item: SplitExcelResultItem) {
|
||||
async function downloadSplitResult(item: SplitResultItem) {
|
||||
const api = getPywebviewApi()
|
||||
if (!item.output_path) {
|
||||
if (!item.downloadUrl) {
|
||||
ElMessage.warning('当前结果没有下载地址')
|
||||
return
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
const yyyy = String(today.getFullYear())
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(today.getDate()).padStart(2, '0')
|
||||
const filename = `${yyyy}${mm}${dd}.xlsx`
|
||||
const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.xlsx`
|
||||
if (api?.save_file_from_url) {
|
||||
const result = await api.save_file_from_url(item.output_path, filename)
|
||||
const result = await api.save_file_from_url(item.downloadUrl, filename)
|
||||
if (result.success) {
|
||||
ElMessage.success(`已保存:${result.path || filename}`)
|
||||
} else if (result.error && result.error !== '用户取消') {
|
||||
@@ -366,12 +337,17 @@ async function downloadSplitResult(item: SplitExcelResultItem) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(item.output_path, '_blank')
|
||||
window.open(item.downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSplitHistory().catch(() => undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-content { display: flex; height: calc(100vh - 56px); }
|
||||
.module-page { min-height: 100vh; background: #1a1a1a; }
|
||||
.main-content { display: flex; min-height: calc(100vh - 56px); height: calc(100vh - 56px); }
|
||||
.left-panel { width: 380px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
||||
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
||||
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||
@@ -379,7 +355,7 @@ async function downloadSplitResult(item: SplitExcelResultItem) {
|
||||
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
||||
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
|
||||
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
||||
.opt-btn, .btn-run, .download { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||
.opt-btn, .btn-run, .download, .btn-delete { border: none; cursor: pointer; transition: all 0.2s ease; }
|
||||
.opt-btn { padding: 8px 16px; font-size: 13px; color: #ccc; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 6px; }
|
||||
.opt-btn.small { padding: 6px 10px; font-size: 12px; }
|
||||
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; }
|
||||
@@ -425,6 +401,8 @@ async function downloadSplitResult(item: SplitExcelResultItem) {
|
||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
||||
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
|
||||
.empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
|
||||
.clean-placeholder { max-height: none; }
|
||||
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; }
|
||||
@@ -433,7 +411,5 @@ async function downloadSplitResult(item: SplitExcelResultItem) {
|
||||
.summary-label { font-size: 12px; color: #8d8d8d; }
|
||||
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; }
|
||||
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; }
|
||||
.download-all { padding: 7px 12px; border: 1px solid #2f5f85; border-radius: 6px; background: rgba(52, 152, 219, 0.14); color: #69b6ff; font-size: 12px; cursor: pointer; }
|
||||
.download-all:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
137
frontend-vue/src/pages/brand/components/BrandTopBar.vue
Normal file
137
frontend-vue/src/pages/brand/components/BrandTopBar.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<header class="top-bar">
|
||||
<div class="logo-area">
|
||||
<span class="app-name">数富AI-亚马逊</span>
|
||||
<a href="/home" class="btn-home">返回首页</a>
|
||||
</div>
|
||||
|
||||
<nav class="nav-tabs">
|
||||
<span class="nav-tab-group">
|
||||
<a v-for="item in navItems" :key="item.key" :href="item.href" class="nav-tab" :class="{ active: active === item.key }">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<div class="top-right"></div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
active: 'brand' | 'dedupe' | 'convert' | 'split'
|
||||
}>()
|
||||
|
||||
const active = props.active
|
||||
|
||||
const navItems = [
|
||||
{ key: 'brand', label: '品牌检测', href: '/brand' },
|
||||
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
|
||||
{ key: 'convert', label: '格式转换', href: '/new_web_source/convert.html' },
|
||||
{ key: 'split', label: '数据拆分', href: '/new_web_source/split.html' },
|
||||
] as const
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-bar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 0 20px;
|
||||
background: #111;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-home {
|
||||
color: #d2d2d2;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-home:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nav-tab-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 92px;
|
||||
height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
color: #8d8d8d;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
color: #b9d6ff;
|
||||
background: rgba(77, 126, 189, 0.14);
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: rgba(77, 126, 189, 0.24);
|
||||
border-color: rgba(91, 148, 219, 0.35);
|
||||
color: #6caeff;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.top-bar {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -39,48 +39,50 @@ export interface BrandTaskMutationResponse {
|
||||
error?: string
|
||||
}
|
||||
|
||||
const API_PREFIX = '/new'
|
||||
|
||||
export function getBrandTaskEventsUrl(taskId: string | number) {
|
||||
return `/api/brand/tasks/${taskId}/events`
|
||||
return `${API_PREFIX}/api/brand/tasks/${taskId}/events`
|
||||
}
|
||||
|
||||
export function getBrandTaskDownloadUrl(taskId: string | number) {
|
||||
return `/api/brand/download/${taskId}`
|
||||
return `${API_PREFIX}/api/brand/download/${taskId}`
|
||||
}
|
||||
|
||||
export function getBrandTemplateXlsxUrl() {
|
||||
return '/static/品牌文档格式_模板.xlsx'
|
||||
return `${API_PREFIX}/static/品牌文档格式_模板.xlsx`
|
||||
}
|
||||
|
||||
export function getBrandTemplateZipUrl() {
|
||||
return '/static/模板2-以文件夹方式上传.zip'
|
||||
return `${API_PREFIX}/static/模板2-以文件夹方式上传.zip`
|
||||
}
|
||||
|
||||
export function expandBrandFolder(folder: string) {
|
||||
return requestPostJson<BrandExpandFolderResponse>('/api/brand/expand-folder', { folder })
|
||||
return requestPostJson<BrandExpandFolderResponse>(`${API_PREFIX}/api/brand/expand-folder`, { folder })
|
||||
}
|
||||
|
||||
export function runBrandNow(paths: string[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>('/api/brand/run', { paths, strategy })
|
||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/run`, { paths, strategy })
|
||||
}
|
||||
|
||||
export function createBrandTask(paths: string[], strategy: string) {
|
||||
return requestPostJson<BrandTaskMutationResponse>('/api/brand/tasks', { paths, strategy })
|
||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks`, { paths, strategy })
|
||||
}
|
||||
|
||||
export function getBrandTasks() {
|
||||
return requestGetJson<BrandTaskListResponse>('/api/brand/tasks')
|
||||
return requestGetJson<BrandTaskListResponse>(`${API_PREFIX}/api/brand/tasks`)
|
||||
}
|
||||
|
||||
export function getBrandTask(taskId: string | number) {
|
||||
return requestGetJson<BrandTaskDetailResponse>(`/api/brand/tasks/${taskId}`)
|
||||
return requestGetJson<BrandTaskDetailResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export function cancelBrandTask(taskId: string | number) {
|
||||
return requestPostJson<BrandTaskMutationResponse>(`/api/brand/tasks/${taskId}/cancel`)
|
||||
return requestPostJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}/cancel`)
|
||||
}
|
||||
|
||||
export function deleteBrandTask(taskId: string | number) {
|
||||
return requestDeleteJson<BrandTaskMutationResponse>(`/api/brand/tasks/${taskId}`)
|
||||
return requestDeleteJson<BrandTaskMutationResponse>(`${API_PREFIX}/api/brand/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export function createBrandTaskEvents(taskId: string | number) {
|
||||
|
||||
@@ -6,27 +6,37 @@ import axios, {
|
||||
type InternalAxiosRequestConfig,
|
||||
} from 'axios'
|
||||
|
||||
export interface ApiSuccess<T> {
|
||||
export interface LegacyApiSuccess<T> {
|
||||
success: true
|
||||
msg?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export interface ApiFailure {
|
||||
export interface LegacyApiFailure {
|
||||
success: false
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = ApiSuccess<T> | ApiFailure
|
||||
export type ApiResponse<T> = LegacyApiSuccess<T> | LegacyApiFailure
|
||||
export interface JavaApiResponse<T> {
|
||||
success: boolean
|
||||
message: string
|
||||
data: T | null
|
||||
}
|
||||
export type RequestOptions<D = unknown> = AxiosRequestConfig<D>
|
||||
|
||||
function extractErrorMessage(error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
const data = error.response?.data
|
||||
|
||||
if (data && typeof data === 'object' && 'error' in data) {
|
||||
if (data && typeof data === 'object') {
|
||||
if ('error' in data && data.error) {
|
||||
return String(data.error)
|
||||
}
|
||||
if ('message' in data && data.message) {
|
||||
return String(data.message)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data === 'string' && data.trim()) {
|
||||
return data
|
||||
@@ -105,3 +115,11 @@ export const requestGetJson = get
|
||||
export const requestPostJson = post
|
||||
export const requestPutJson = put
|
||||
export const requestDeleteJson = del
|
||||
|
||||
export async function unwrapJavaResponse<T>(promise: Promise<JavaApiResponse<T>>) {
|
||||
const response = await promise
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || '请求失败')
|
||||
}
|
||||
return response.data as T
|
||||
}
|
||||
|
||||
162
frontend-vue/src/shared/api/java-modules.ts
Normal file
162
frontend-vue/src/shared/api/java-modules.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { del, get, http, post, type JavaApiResponse, unwrapJavaResponse } from '@/shared/api/http'
|
||||
|
||||
const JAVA_API_PREFIX = '/newApi/api'
|
||||
|
||||
export interface UploadedFileRef {
|
||||
fileKey: string
|
||||
originalFilename?: string
|
||||
}
|
||||
|
||||
export interface UploadFileVo {
|
||||
fileKey: string
|
||||
originalFilename: string
|
||||
localPath: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface DedupeResultItem {
|
||||
resultId?: number
|
||||
sourceFilename: string
|
||||
outputFilename?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
downloadUrl?: string
|
||||
}
|
||||
|
||||
export interface DedupeRunVo {
|
||||
total: number
|
||||
successCount: number
|
||||
failedCount: number
|
||||
items: DedupeResultItem[]
|
||||
}
|
||||
|
||||
export interface DedupeHistoryVo {
|
||||
items: DedupeResultItem[]
|
||||
}
|
||||
|
||||
export interface DedupeRunRequest {
|
||||
files: UploadedFileRef[]
|
||||
selectedColumns: string[]
|
||||
keepIntegerIds: boolean
|
||||
keepUnderscoreIds: boolean
|
||||
}
|
||||
|
||||
export interface SplitResultItem {
|
||||
resultId?: number
|
||||
sourceFilename: string
|
||||
outputFilename?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
rowCount?: number
|
||||
downloadUrl?: string
|
||||
}
|
||||
|
||||
export interface SplitRunVo {
|
||||
total: number
|
||||
successCount: number
|
||||
failedCount: number
|
||||
items: SplitResultItem[]
|
||||
}
|
||||
|
||||
export interface SplitHistoryVo {
|
||||
items: SplitResultItem[]
|
||||
}
|
||||
|
||||
export interface SplitRunRequest {
|
||||
files: UploadedFileRef[]
|
||||
selectedColumns: string[]
|
||||
splitMode: 'rows_per_file' | 'parts'
|
||||
rowsPerFile?: number
|
||||
parts?: number
|
||||
}
|
||||
|
||||
export interface ConvertResultItem {
|
||||
resultId?: number
|
||||
sourceFilename: string
|
||||
outputFilename?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
downloadUrl?: string
|
||||
}
|
||||
|
||||
export interface ConvertRunVo {
|
||||
total: number
|
||||
successCount: number
|
||||
failedCount: number
|
||||
items: ConvertResultItem[]
|
||||
}
|
||||
|
||||
export interface ConvertHistoryVo {
|
||||
items: ConvertResultItem[]
|
||||
}
|
||||
|
||||
export interface ConvertRunRequest {
|
||||
files: UploadedFileRef[]
|
||||
templateId: string
|
||||
}
|
||||
|
||||
export interface ConvertTemplateVo {
|
||||
id: string
|
||||
templateCode: string
|
||||
templateName: string
|
||||
outputFilename: string
|
||||
isDefault?: boolean
|
||||
builtIn?: boolean
|
||||
}
|
||||
|
||||
export interface ExcelInfoVo {
|
||||
headers: string[]
|
||||
totalRows: number
|
||||
}
|
||||
|
||||
export function getExcelInfo(fileKey: string) {
|
||||
return unwrapJavaResponse(get<JavaApiResponse<ExcelInfoVo>>(`${JAVA_API_PREFIX}/files/excel-info`, {
|
||||
params: { fileKey },
|
||||
}))
|
||||
}
|
||||
|
||||
export function runDedupe(request: DedupeRunRequest) {
|
||||
return unwrapJavaResponse(post<JavaApiResponse<DedupeRunVo>, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, request))
|
||||
}
|
||||
|
||||
export function getDedupeHistory() {
|
||||
return unwrapJavaResponse(get<JavaApiResponse<DedupeHistoryVo>>(`${JAVA_API_PREFIX}/dedupe/history`))
|
||||
}
|
||||
|
||||
export function deleteDedupeHistory(resultId: number) {
|
||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`))
|
||||
}
|
||||
|
||||
export function runSplit(request: SplitRunRequest) {
|
||||
return unwrapJavaResponse(post<JavaApiResponse<SplitRunVo>, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, request))
|
||||
}
|
||||
|
||||
export function getSplitHistory() {
|
||||
return unwrapJavaResponse(get<JavaApiResponse<SplitHistoryVo>>(`${JAVA_API_PREFIX}/split/history`))
|
||||
}
|
||||
|
||||
export function deleteSplitHistory(resultId: number) {
|
||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/split/history/${resultId}`))
|
||||
}
|
||||
|
||||
export function getConvertTemplates() {
|
||||
return unwrapJavaResponse(get<JavaApiResponse<ConvertTemplateVo[]>>(`${JAVA_API_PREFIX}/convert/templates`))
|
||||
}
|
||||
|
||||
export function runConvert(request: ConvertRunRequest) {
|
||||
return unwrapJavaResponse(post<JavaApiResponse<ConvertRunVo>, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, request))
|
||||
}
|
||||
|
||||
export function getConvertHistory() {
|
||||
return unwrapJavaResponse(get<JavaApiResponse<ConvertHistoryVo>>(`${JAVA_API_PREFIX}/convert/history`))
|
||||
}
|
||||
|
||||
export function deleteConvertHistory(resultId: number) {
|
||||
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/convert/history/${resultId}`))
|
||||
}
|
||||
|
||||
export function getJavaDownloadUrl(path: string) {
|
||||
return path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||
}
|
||||
|
||||
export { JAVA_API_PREFIX, http }
|
||||
@@ -1,149 +1,24 @@
|
||||
export interface SplitExcelPayload {
|
||||
paths: string[]
|
||||
selected_columns: string[]
|
||||
split_mode: 'rows_per_file' | 'parts'
|
||||
rows_per_file?: number
|
||||
parts?: number
|
||||
output_dir?: string
|
||||
}
|
||||
|
||||
export interface SplitExcelResultItem {
|
||||
result_id?: number
|
||||
source_path: string
|
||||
output_path?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
row_count?: number
|
||||
}
|
||||
|
||||
export interface SplitExcelSummary {
|
||||
total: number
|
||||
success_count: number
|
||||
failed_count: number
|
||||
output_dir?: string
|
||||
}
|
||||
|
||||
export interface SplitExcelResponse {
|
||||
success: boolean
|
||||
summary?: SplitExcelSummary
|
||||
items?: SplitExcelResultItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ExcelInfoResponse {
|
||||
success: boolean
|
||||
headers?: string[]
|
||||
total_rows?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface CleanExcelPayload {
|
||||
paths: string[]
|
||||
selected_columns: string[]
|
||||
keep_integer_ids?: boolean
|
||||
keep_underscore_ids?: boolean
|
||||
output_dir?: string
|
||||
}
|
||||
|
||||
export interface CleanExcelResultItem {
|
||||
result_id?: number
|
||||
source_path: string
|
||||
output_path?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ConvertTxtPayload {
|
||||
paths: string[]
|
||||
output_dir?: string
|
||||
template_id: string
|
||||
}
|
||||
|
||||
export interface ConvertTxtTemplateItem {
|
||||
id: string
|
||||
label: string
|
||||
output_filename: string
|
||||
is_default?: boolean
|
||||
built_in?: boolean
|
||||
}
|
||||
|
||||
export interface ConvertTxtResultItem {
|
||||
result_id?: number
|
||||
source_path: string
|
||||
output_path?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ConvertTxtSummary {
|
||||
total: number
|
||||
success_count: number
|
||||
failed_count: number
|
||||
output_dir?: string
|
||||
}
|
||||
|
||||
export interface ConvertTxtResponse {
|
||||
success: boolean
|
||||
summary?: ConvertTxtSummary
|
||||
items?: ConvertTxtResultItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface CleanExcelSummary {
|
||||
total: number
|
||||
success_count: number
|
||||
failed_count: number
|
||||
output_dir?: string
|
||||
}
|
||||
|
||||
export interface CleanExcelResponse {
|
||||
success: boolean
|
||||
summary?: CleanExcelSummary
|
||||
items?: CleanExcelResultItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ConvertTxtHistoryResponse {
|
||||
success: boolean
|
||||
items?: ConvertTxtResultItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface SplitExcelHistoryResponse {
|
||||
success: boolean
|
||||
items?: SplitExcelResultItem[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface CleanExcelHistoryResponse {
|
||||
success: boolean
|
||||
items?: CleanExcelResultItem[]
|
||||
error?: string
|
||||
export interface UploadedJavaFile {
|
||||
fileKey: string
|
||||
originalFilename: string
|
||||
localPath: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface PywebviewApi {
|
||||
select_brand_xlsx_files?: () => Promise<string[]>
|
||||
select_clean_xlsx_files?: () => Promise<string[]>
|
||||
select_brand_folder?: () => Promise<string | null>
|
||||
select_clean_folder?: () => Promise<string | null>
|
||||
close?: () => Promise<void>
|
||||
minimize?: () => Promise<void>
|
||||
maximize?: () => Promise<void>
|
||||
toggle_maximize?: () => Promise<void>
|
||||
save_image?: (urlOrData: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
save_image_to_folder?: (urlOrData: string, dirPath: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
select_folder?: () => Promise<string | null>
|
||||
get_excel_headers?: (filePath: string) => Promise<{ success: boolean; headers?: string[]; error?: string }>
|
||||
get_dedupe_history?: () => Promise<CleanExcelHistoryResponse>
|
||||
get_convert_history?: () => Promise<ConvertTxtHistoryResponse>
|
||||
get_split_history?: () => Promise<SplitExcelHistoryResponse>
|
||||
delete_history_item?: (moduleType: string, resultId: number) => Promise<{ success: boolean; error?: string }>
|
||||
get_excel_info?: (filePath: string) => Promise<ExcelInfoResponse>
|
||||
clean_excel_files?: (payload: CleanExcelPayload) => Promise<CleanExcelResponse>
|
||||
split_excel_files?: (payload: SplitExcelPayload) => Promise<SplitExcelResponse>
|
||||
list_txt_templates?: () => Promise<ConvertTxtTemplateItem[]>
|
||||
import_txt_template?: (name?: string) => Promise<{ success: boolean; template?: ConvertTxtTemplateItem; error?: string }>
|
||||
set_default_txt_template?: (templateId: string) => Promise<{ success: boolean; error?: string }>
|
||||
delete_txt_template?: (templateId: string) => Promise<{ success: boolean; error?: string }>
|
||||
convert_excel_to_uk_txt?: (payload: ConvertTxtPayload) => Promise<ConvertTxtResponse>
|
||||
open_path?: (path: string) => Promise<{ success: boolean; error?: string }>
|
||||
select_brand_xlsx_files?: () => Promise<string[]>
|
||||
select_brand_folder?: () => Promise<string | null>
|
||||
upload_file_to_java?: (filePath: string) => Promise<{ success: boolean; message?: string; data?: UploadedJavaFile; error?: string }>
|
||||
save_file_from_url?: (url: string, filename: string) => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
save_template_xlsx?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
save_template_zip?: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
save_file_from_url?: (url: string, filename: string) => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -154,8 +29,26 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
let cachedPywebviewApi: PywebviewApi | undefined
|
||||
let pywebviewReadyBound = false
|
||||
|
||||
function syncPywebviewApi() {
|
||||
cachedPywebviewApi = window.pywebview?.api
|
||||
return cachedPywebviewApi
|
||||
}
|
||||
|
||||
function bindPywebviewReady() {
|
||||
if (pywebviewReadyBound || typeof window === 'undefined' || !window.addEventListener) return
|
||||
pywebviewReadyBound = true
|
||||
window.addEventListener('pywebviewready', () => {
|
||||
syncPywebviewApi()
|
||||
})
|
||||
}
|
||||
|
||||
bindPywebviewReady()
|
||||
|
||||
export function getPywebviewApi() {
|
||||
return window.pywebview?.api
|
||||
return syncPywebviewApi() || cachedPywebviewApi
|
||||
}
|
||||
|
||||
export function hasPywebview() {
|
||||
|
||||
@@ -7,7 +7,7 @@ import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
base: '/',
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
@@ -29,26 +29,15 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/login': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/logout': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/static': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
'/newApi/api': {
|
||||
target: 'http://127.0.0.1:18080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/newApi/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
outDir: 'new_web_source',
|
||||
emptyOutDir: true,
|
||||
cssCodeSplit: true,
|
||||
rollupOptions: {
|
||||
|
||||
BIN
source_code/__pycache__/app.cpython-314.pyc
Normal file
BIN
source_code/__pycache__/app.cpython-314.pyc
Normal file
Binary file not shown.
BIN
source_code/__pycache__/config.cpython-314.pyc
Normal file
BIN
source_code/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
source_code/__pycache__/main.cpython-314.pyc
Normal file
BIN
source_code/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user