环境搭建配置

This commit is contained in:
super
2026-03-21 10:00:16 +08:00
parent 82ccd9a8ea
commit 4b83bf76b4
55 changed files with 8801 additions and 616 deletions

4
.gitignore vendored
View File

@@ -3,6 +3,7 @@ frontend-vue/node_modules/
# frontend-vue build output # frontend-vue build output
frontend-vue/dist/ frontend-vue/dist/
frontend-vue/new_web_source
# generated type declarations # generated type declarations
frontend-vue/auto-imports.d.ts frontend-vue/auto-imports.d.ts
@@ -28,3 +29,6 @@ backend-java/src/main/resources/application-local.yml
# backend-java IDE files # backend-java IDE files
backend-java/.idea/ backend-java/.idea/
backend-java/*.iml backend-java/*.iml
# desktop app
desktop/

BIN
1.xlsx Normal file

Binary file not shown.

BIN
5.xlsx Normal file

Binary file not shown.

12
app/.env Normal file
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -37,7 +37,7 @@ app = create_app()
def run_app(host='127.0.0.1', port=5123): 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) app.run(host=host, port=port, threaded=True, use_reloader=False)

View File

@@ -14,6 +14,7 @@ from config import mysql_host, mysql_user, mysql_password, mysql_database
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, 'static') STATIC_DIR = os.path.join(BASE_DIR, 'static')
ASSETS_DIR = os.path.join(BASE_DIR, 'assets')
def get_db(): def get_db():
@@ -42,6 +43,22 @@ def _render_html(template_name: str, **context):
return render_template_string(content, **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(): def _is_session_user_valid():
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False""" """校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
uid = session.get('user_id') uid = session.get('user_id')

File diff suppressed because one or more lines are too long

1
app/assets/convert.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
app/assets/dedupe.js Normal file

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

1
app/assets/split.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,8 @@
主页面蓝图首页、home、图片工作台、品牌页、静态文件、Logo 主页面蓝图首页、home、图片工作台、品牌页、静态文件、Logo
""" """
import os import os
from flask import Blueprint, send_file import requests
from flask import Blueprint, Response, request, send_file
from app_common import ( from app_common import (
get_db, get_db,
@@ -16,7 +17,9 @@ from app_common import (
) )
from flask import redirect, url_for, session 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__) 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']) @main_bp.route('/logo.jpg', methods=['GET'])
def get_logo_image(): def get_logo_image():
return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg') return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg')

Binary file not shown.

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
_base_url = os.getenv("base_url","http://159.75.121.33:15124") _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 配置
mysql_host = os.getenv("mysql_host") mysql_host = os.getenv("mysql_host")
@@ -16,6 +17,7 @@ mysql_database = os.getenv("mysql_database","aiimage")
proxy_url = os.getenv("proxy_url") proxy_url = os.getenv("proxy_url")
proxy_mode = int(os.getenv("proxy_mode",1)) proxy_mode = int(os.getenv("proxy_mode",1))
client_name=os.getenv("client_name") + ".exe" client_name=os.getenv("client_name") + ".exe"
cache_path = "./user_data" cache_path = "./user_data"

View File

@@ -1,7 +1,7 @@
""" """
基于 pywebview 实现的跨平台 UI需先登录方可使用 基于 pywebview 实现的跨平台 UI需先登录方可使用
""" """
from config import cache_path,debug,version from config import cache_path,debug,version,java_api_base
import datetime import datetime
import json import json
import sys import sys
@@ -15,6 +15,7 @@ if not debug:
import webview import webview
import threading import threading
import time import time
import requests
with open("version.txt","w",encoding="utf-8") as file: 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__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
APP_URL = "http://127.0.0.1:5123" APP_URL = "http://127.0.0.1:5123"
PORT = 5123 PORT = 5123
JAVA_API_BASE = java_api_base
def generate_images(params): def generate_images(params):
@@ -143,6 +145,28 @@ class WindowAPI:
) )
return result[0] if result else '' 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'): def save_file_from_url(self, url, default_filename='download.zip'):
"""弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。""" """弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。"""
if not url or not url.strip(): if not url or not url.strip():
@@ -332,7 +356,7 @@ def main():
easy_drag=True easy_drag=True
) )
api = WindowAPI(window) 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( webview.start(
debug=True, debug=True,
storage_path=cache_path, storage_path=cache_path,

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

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

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

Binary file not shown.

1
app/version.txt Normal file
View 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

View File

@@ -5,6 +5,11 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain; 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 @Configuration
public class SecurityConfig { public class SecurityConfig {
@@ -12,9 +17,25 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http return http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.httpBasic(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults())
.build(); .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;
}
} }

View File

@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.file.controller; package com.nanri.aiimage.modules.file.controller;
import com.nanri.aiimage.common.api.ApiResponse; 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.model.vo.UploadFileVo;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService; import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
import io.swagger.v3.oas.annotations.Operation; 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.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; 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.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -46,4 +49,14 @@ public class FileUploadController {
MultipartFile file) throws Exception { MultipartFile file) throws Exception {
return ApiResponse.success(localFileStorageService.saveTempFile(file)); 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));
}
} }

View File

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

View File

@@ -2,14 +2,26 @@ package com.nanri.aiimage.modules.file.service;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.StorageProperties; 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 com.nanri.aiimage.modules.file.model.vo.UploadFileVo;
import lombok.RequiredArgsConstructor; 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.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -36,4 +48,73 @@ public class LocalFileStorageService {
vo.setSize(file.getSize()); vo.setSize(file.getSize());
return vo; 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+", " ");
}
} }

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="main-content"> <div class="page-shell module-page">
<aside class="left-panel"> <BrandTopBar active="convert" />
<div class="main-content">
<aside class="left-panel">
<div class="section-title">选择文件</div> <div class="section-title">选择文件</div>
<div class="upload-zone"> <div class="upload-zone">
<div class="hint">选择需要转换的 Excel 文件或文件夹将按所选模板生成 txt 文件</div> <div class="hint">选择需要转换的 Excel 文件或文件夹将按所选模板生成 txt 文件</div>
@@ -9,14 +12,6 @@
<button type="button" class="opt-btn" @click="selectConvertFolder">选择文件夹</button> <button type="button" class="opt-btn" @click="selectConvertFolder">选择文件夹</button>
</div> </div>
<el-alert
v-if="!hasPywebviewSupport"
class="desktop-alert"
type="warning"
:closable="false"
title="当前环境未检测到 pywebview文件选择与本地格式转换能力需要在桌面端使用。"
/>
<div class="selected-files clean-placeholder"> <div class="selected-files clean-placeholder">
<template v-if="convertSelectedPaths.length"> <template v-if="convertSelectedPaths.length">
<span v-for="path in convertDisplayPaths" :key="path">{{ path }}</span> <span v-for="path in convertDisplayPaths" :key="path">{{ path }}</span>
@@ -29,10 +24,6 @@
</div> </div>
<div class="section-title">模板选择</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"> <div class="option-group">
<label <label
v-for="template in convertTemplates" v-for="template in convertTemplates"
@@ -44,28 +35,15 @@
<div class="template-item-content"> <div class="template-item-content">
<div> <div>
<span class="label"> <span class="label">
{{ template.label }} {{ template.templateName }}
<span v-if="template.is_default" class="template-tag">默认</span> <span v-if="template.isDefault" class="template-tag">默认</span>
<span v-else-if="template.built_in" class="template-tag muted">内置</span> <span v-else-if="template.builtIn" class="template-tag muted">内置</span>
</span> </span>
<div class="desc">输出文件{{ template.output_filename }}</div> <div class="desc">输出文件{{ template.outputFilename }}</div>
</div> </div>
<button
v-if="!template.built_in"
type="button"
class="template-delete-btn"
@click.stop.prevent="deleteConvertTemplate(template.id)"
>
删除
</button>
</div> </div>
</label> </label>
</div> </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="section-title">转换说明</div>
<div class="option-group"> <div class="option-group">
@@ -73,7 +51,7 @@
<input type="checkbox" checked disabled /> <input type="checkbox" checked disabled />
<div> <div>
<span class="label">按当前模板输出</span> <span class="label">按当前模板输出</span>
<div class="desc">当前模板{{ currentConvertTemplate?.label || '未加载模板' }}</div> <div class="desc">当前模板{{ currentConvertTemplate?.templateName || '未加载模板' }}</div>
</div> </div>
</label> </label>
@@ -107,11 +85,11 @@
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="summary-label">成功结果</span> <span class="summary-label">成功结果</span>
<strong>{{ convertSummary.success_count }}</strong> <strong>{{ convertSummary.successCount }}</strong>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="summary-label">失败文件</span> <span class="summary-label">失败文件</span>
<strong>{{ convertSummary.failed_count }}</strong> <strong>{{ convertSummary.failedCount }}</strong>
</div> </div>
</div> </div>
@@ -127,12 +105,12 @@
<ul v-else class="task-list clean-result-list"> <ul v-else class="task-list clean-result-list">
<li <li
v-for="item in convertResultItems" 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" class="task-item"
> >
<div class="left"> <div class="left">
<span class="id" :title="item.source_path">{{ item.source_path || '-' }}</span> <span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
<div v-if="item.output_path" class="files">结果下载地址已生成</div> <div v-if="item.outputFilename" class="files">输出文件{{ item.outputFilename }}</div>
<div v-else-if="item.error" class="files">错误信息{{ item.error }}</div> <div v-else-if="item.error" class="files">错误信息{{ item.error }}</div>
</div> </div>
@@ -141,7 +119,7 @@
{{ item.success ? '已完成' : '失败' }} {{ item.success ? '已完成' : '失败' }}
</span> </span>
<button <button
v-if="item.success && item.output_path" v-if="item.success && item.downloadUrl"
type="button" type="button"
class="download" class="download"
@click="downloadConvertResult(item)" @click="downloadConvertResult(item)"
@@ -149,10 +127,10 @@
下载文件 下载文件
</button> </button>
<button <button
v-if="item.result_id" v-if="item.resultId"
type="button" type="button"
class="btn-delete" class="btn-delete"
@click="deleteConvertHistory(item.result_id)" @click="deleteConvertHistoryRecord(item.resultId)"
> >
删除 删除
</button> </button>
@@ -162,43 +140,50 @@
</div> </div>
</div> </div>
</section> </section>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import { expandBrandFolder } from '@/shared/api/brand' 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 convertSelectedPaths = ref<string[]>([])
const convertUploadedFiles = ref<UploadedJavaFile[]>([])
const convertRunning = ref(false) const convertRunning = ref(false)
const convertOutputDir = ref('') const convertResultItems = ref<ConvertResultItem[]>([])
const convertResultItems = ref<ConvertTxtResultItem[]>([]) const convertSummary = ref<ConvertRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
const convertSummary = ref({ total: 0, success_count: 0, failed_count: 0 }) const convertTemplates = ref<ConvertTemplateVo[]>([])
const convertTemplates = ref<ConvertTxtTemplateItem[]>([]) const convertTemplateId = ref('')
const convertTemplateId = ref('uk_offer')
const convertTemplateName = ref('')
const convertDisplayPaths = computed(() => convertSelectedPaths.value.slice(0, 8)) const convertDisplayPaths = computed(() => convertSelectedPaths.value.slice(0, 8))
const currentConvertTemplate = computed( const currentConvertTemplate = computed(
() => convertTemplates.value.find((item) => item.id === convertTemplateId.value) || null, () => convertTemplates.value.find((item) => item.id === convertTemplateId.value) || null,
) )
function shorten(value: string, maxLength: number) { async function uploadPathsToJava(paths: string[]) {
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value const api = getPywebviewApi()
} if (!api?.upload_file_to_java) {
throw new Error('当前桌面端未提供文件上传桥接能力')
function resetConvertResults() { }
convertSummary.value = { total: 0, success_count: 0, failed_count: 0 } const uploaded: UploadedJavaFile[] = []
convertOutputDir.value = '' 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() { async function loadConvertTemplates() {
const api = getPywebviewApi() const templates = await getConvertTemplates()
const templates = await api?.list_txt_templates?.() convertTemplates.value = templates || []
convertTemplates.value = Array.isArray(templates) ? templates : [] const defaultTemplate = convertTemplates.value.find((item) => item.isDefault)
const defaultTemplate = convertTemplates.value.find((item) => item.is_default)
if (defaultTemplate) { if (defaultTemplate) {
convertTemplateId.value = defaultTemplate.id convertTemplateId.value = defaultTemplate.id
return return
@@ -208,79 +193,23 @@ async function loadConvertTemplates() {
} }
} }
async function importConvertTemplate() { async function handleSelectedPaths(paths: string[], successMessage: string) {
const api = getPywebviewApi() convertSelectedPaths.value = paths
if (!api?.import_txt_template) { convertUploadedFiles.value = await uploadPathsToJava(paths)
ElMessage.warning('当前环境不支持模板上传,请在桌面端打开') ElMessage.success(successMessage)
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 selectConvertFiles() { async function selectConvertFiles() {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.select_clean_xlsx_files) { if (!api?.select_brand_xlsx_files) {
ElMessage.warning('当前环境不支持文件选择,请在桌面端打开') ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
return return
} }
try { try {
const paths = await api.select_clean_xlsx_files() const paths = await api.select_brand_xlsx_files()
if (!paths?.length) return if (!paths?.length) return
convertSelectedPaths.value = paths await handleSelectedPaths(paths, `已选择 ${paths.length} 个待转换文件`)
resetConvertResults()
ElMessage.success(`已选择 ${paths.length} 个待转换文件`)
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '选择失败') ElMessage.error(error instanceof Error ? error.message : '选择失败')
} }
@@ -288,20 +217,18 @@ async function selectConvertFiles() {
async function selectConvertFolder() { async function selectConvertFolder() {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.select_clean_folder) { if (!api?.select_brand_folder) {
ElMessage.warning('当前环境不支持文件夹选择,请在桌面端打开') ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
return return
} }
try { try {
const folder = await api.select_clean_folder() const folder = await api.select_brand_folder()
if (!folder) return if (!folder) return
const result = await expandBrandFolder(folder) const result = await expandBrandFolder(folder)
if (result.success && result.paths?.length) { if (result.success && result.paths?.length) {
convertSelectedPaths.value = result.paths await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
resetConvertResults()
ElMessage.success(`已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
return return
} }
@@ -312,34 +239,23 @@ async function selectConvertFolder() {
} }
async function submitConvertRun() { async function submitConvertRun() {
const api = getPywebviewApi() if (!convertUploadedFiles.value.length) {
if (!api?.convert_excel_to_uk_txt) { ElMessage.warning('请先选择待转换 Excel 文件或文件夹')
ElMessage.warning('当前环境不支持格式转换,请在桌面端打开')
return return
} }
if (!convertSelectedPaths.value.length) { if (!convertTemplateId.value) {
ElMessage.warning('请先选择待转换 Excel 文件或文件夹') ElMessage.warning('请先选择模板')
return return
} }
try { try {
convertRunning.value = true convertRunning.value = true
const result = await api.convert_excel_to_uk_txt({ const result = await runConvert({
paths: convertSelectedPaths.value, files: convertUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
template_id: convertTemplateId.value, templateId: convertTemplateId.value,
}) })
convertSummary.value = result
if (!result.success) { convertResultItems.value = result.items || []
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,
}
await loadConvertHistory() await loadConvertHistory()
ElMessage.success('格式转换完成') ElMessage.success('格式转换完成')
} catch (error) { } catch (error) {
@@ -349,36 +265,41 @@ async function submitConvertRun() {
} }
} }
async function deleteConvertHistory(resultId: number) { async function loadConvertHistory() {
const api = getPywebviewApi() try {
if (!api?.delete_history_item) { const response = await getConvertHistory()
ElMessage.warning('当前环境不支持删除历史') convertResultItems.value = response.items || []
return 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 || [],
}
} catch {
// ignore history load errors
} }
const result = await api.delete_history_item('convert', resultId)
if (!result.success) {
ElMessage.error(result.error || '删除失败')
return
}
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() const api = getPywebviewApi()
if (!item.output_path) { if (!item.downloadUrl) {
ElMessage.warning('当前结果没有下载地址') ElMessage.warning('当前结果没有下载地址')
return return
} }
const today = new Date() const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.txt`
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`
if (api?.save_file_from_url) { 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) { if (result.success) {
ElMessage.success(`已保存:${result.path || filename}`) ElMessage.success(`已保存:${result.path || filename}`)
} else if (result.error && result.error !== '用户取消') { } else if (result.error && result.error !== '用户取消') {
@@ -387,25 +308,7 @@ async function downloadConvertResult(item: ConvertTxtResultItem) {
return return
} }
window.open(item.output_path, '_blank') window.open(item.downloadUrl, '_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
}
} }
onMounted(() => { onMounted(() => {
@@ -415,7 +318,8 @@ onMounted(() => {
</script> </script>
<style scoped> <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; } .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; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
@@ -423,29 +327,22 @@ onMounted(() => {
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; } .upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; } .hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; } .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 { 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.small { padding: 6px 10px; font-size: 12px; }
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; } .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 { 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; } .selected-files span { display: block; margin: 4px 0; word-break: break-all; }
.more-line { color: #b8c1cc; } .more-line { color: #b8c1cc; }
.option-group { margin-bottom: 20px; } .option-group { margin-bottom: 20px; }
.template-toolbar { display: flex; gap: 10px; margin-bottom: 12px; } .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-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; }
.template-item-content { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; } .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:hover, .radio-item.active { background: #2a2a2a; border-color: #3a3a3a; }
.radio-item input { margin-top: 3px; } .radio-item input { margin-top: 3px; }
.label { font-weight: 500; color: #e0e0e0; font-size: 13px; } .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; } .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; } .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 { padding: 10px 22px; background: #3498db; color: #fff; border-radius: 8px; font-size: 14px; }
.btn-run:hover { background: #2980b9; } .btn-run:hover { background: #2980b9; }
@@ -464,6 +361,8 @@ onMounted(() => {
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; } .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 { 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); } .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; } .empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
.clean-placeholder { max-height: none; } .clean-placeholder { max-height: none; }
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; } .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; } .summary-label { font-size: 12px; color: #8d8d8d; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; } .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; } .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; } } @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> </style>

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="main-content"> <div class="page-shell module-page">
<aside class="left-panel"> <BrandTopBar active="dedupe" />
<div class="main-content">
<aside class="left-panel">
<div class="section-title">选择文件</div> <div class="section-title">选择文件</div>
<div class="upload-zone"> <div class="upload-zone">
<div class="hint">选择需要清洗的 Excel 文件或文件夹后续将按所选规则输出处理结果</div> <div class="hint">选择需要清洗的 Excel 文件或文件夹后续将按所选规则输出处理结果</div>
@@ -9,14 +12,6 @@
<button type="button" class="opt-btn" @click="selectCleanFolder">选择文件夹</button> <button type="button" class="opt-btn" @click="selectCleanFolder">选择文件夹</button>
</div> </div>
<el-alert
v-if="!hasPywebviewSupport"
class="desktop-alert"
type="warning"
:closable="false"
title="当前环境未检测到 pywebview文件选择与 Excel 表头读取能力需要在桌面端使用。"
/>
<div class="selected-files clean-placeholder"> <div class="selected-files clean-placeholder">
<template v-if="cleanSelectedPaths.length"> <template v-if="cleanSelectedPaths.length">
<span v-for="path in cleanDisplayPaths" :key="path">{{ path }}</span> <span v-for="path in cleanDisplayPaths" :key="path">{{ path }}</span>
@@ -95,11 +90,11 @@
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="summary-label">成功结果</span> <span class="summary-label">成功结果</span>
<strong>{{ cleanSummary.success_count }}</strong> <strong>{{ cleanSummary.successCount }}</strong>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="summary-label">失败文件</span> <span class="summary-label">失败文件</span>
<strong>{{ cleanSummary.failed_count }}</strong> <strong>{{ cleanSummary.failedCount }}</strong>
</div> </div>
</div> </div>
@@ -113,10 +108,10 @@
</div> </div>
<ul v-else class="task-list clean-result-list"> <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"> <div class="left">
<span class="id" :title="item.source_path">{{ item.source_path || '-' }}</span> <span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
<div v-if="item.output_path" class="files">结果下载地址已生成</div> <div v-if="item.outputFilename" class="files">输出文件{{ item.outputFilename }}</div>
<div v-else-if="item.error" class="files">错误信息{{ item.error }}</div> <div v-else-if="item.error" class="files">错误信息{{ item.error }}</div>
</div> </div>
@@ -125,7 +120,7 @@
{{ item.success ? '已完成' : '失败' }} {{ item.success ? '已完成' : '失败' }}
</span> </span>
<button <button
v-if="item.success && item.output_path" v-if="item.success && item.downloadUrl"
type="button" type="button"
class="download" class="download"
@click="downloadCleanResult(item)" @click="downloadCleanResult(item)"
@@ -133,10 +128,10 @@
下载文件 下载文件
</button> </button>
<button <button
v-if="item.result_id" v-if="item.resultId"
type="button" type="button"
class="btn-delete" class="btn-delete"
@click="deleteCleanHistory(item.result_id)" @click="deleteCleanHistoryRecord(item.resultId)"
> >
删除 删除
</button> </button>
@@ -146,31 +141,29 @@
</div> </div>
</div> </div>
</section> </section>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import { expandBrandFolder } from '@/shared/api/brand' 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 cleanAvailableColumns = ref<string[]>([])
const cleanSelectedColumns = ref<string[]>([]) const cleanSelectedColumns = ref<string[]>([])
const cleanSelectedPaths = ref<string[]>([]) const cleanSelectedPaths = ref<string[]>([])
const cleanUploadedFiles = ref<UploadedJavaFile[]>([])
const cleanKeepIntegerIds = ref(true) const cleanKeepIntegerIds = ref(true)
const cleanKeepUnderscoreIds = ref(true) const cleanKeepUnderscoreIds = ref(true)
const cleanRunning = ref(false) const cleanRunning = ref(false)
const cleanOutputDir = ref('') const cleanResultItems = ref<DedupeResultItem[]>([])
const cleanResultItems = ref<CleanExcelResultItem[]>([]) const cleanSummary = ref<DedupeRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
const cleanSummary = ref({ total: 0, success_count: 0, failed_count: 0 })
const cleanDisplayPaths = computed(() => cleanSelectedPaths.value.slice(0, 8)) 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() { function selectAllCleanColumns() {
cleanSelectedColumns.value = [...cleanAvailableColumns.value] cleanSelectedColumns.value = [...cleanAvailableColumns.value]
} }
@@ -179,19 +172,26 @@ function clearAllCleanColumns() {
cleanSelectedColumns.value = [] cleanSelectedColumns.value = []
} }
async function loadCleanHeaders(filePath: string) { async function uploadPathsToJava(paths: string[]) {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.get_excel_headers) { if (!api?.upload_file_to_java) {
ElMessage.warning('当前环境不支持 Excel 表头读取,请在桌面端打开') throw new Error('当前桌面端未提供文件上传桥接能力')
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
}
cleanSummary.value = { total: 0, success_count: 0, failed_count: 0 } async function loadCleanHeaders(fileKey: string) {
cleanOutputDir.value = '' const result = await getExcelInfo(fileKey)
if (!result.headers?.length) {
const result = await api.get_excel_headers(filePath) ElMessage.error('读取 Excel 表头失败')
if (!result.success || !result.headers?.length) {
ElMessage.error(result.error || '读取 Excel 表头失败')
cleanAvailableColumns.value = [] cleanAvailableColumns.value = []
cleanSelectedColumns.value = [] cleanSelectedColumns.value = []
return return
@@ -207,19 +207,26 @@ async function loadCleanHeaders(filePath: string) {
cleanSelectedColumns.value = preferredHeaders.filter((header) => orderedHeaders.includes(header)) 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() { async function selectCleanFiles() {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.select_clean_xlsx_files) { if (!api?.select_brand_xlsx_files) {
ElMessage.warning('当前环境不支持文件选择,请在桌面端打开') ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
return return
} }
try { try {
const paths = await api.select_clean_xlsx_files() const paths = await api.select_brand_xlsx_files()
if (!paths?.length) return if (!paths?.length) return
cleanSelectedPaths.value = paths await handleSelectedPaths(paths, `已选择 ${paths.length} 个待清洗文件`)
await loadCleanHeaders(paths[0])
ElMessage.success(`已选择 ${paths.length} 个待清洗文件`)
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '选择失败') ElMessage.error(error instanceof Error ? error.message : '选择失败')
} }
@@ -227,20 +234,18 @@ async function selectCleanFiles() {
async function selectCleanFolder() { async function selectCleanFolder() {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.select_clean_folder) { if (!api?.select_brand_folder) {
ElMessage.warning('当前环境不支持文件夹选择,请在桌面端打开') ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
return return
} }
try { try {
const folder = await api.select_clean_folder() const folder = await api.select_brand_folder()
if (!folder) return if (!folder) return
const result = await expandBrandFolder(folder) const result = await expandBrandFolder(folder)
if (result.success && result.paths?.length) { if (result.success && result.paths?.length) {
cleanSelectedPaths.value = result.paths await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
await loadCleanHeaders(result.paths[0])
ElMessage.success(`已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
return return
} }
@@ -251,12 +256,7 @@ async function selectCleanFolder() {
} }
async function submitCleanRun() { async function submitCleanRun() {
const api = getPywebviewApi() if (!cleanUploadedFiles.value.length) {
if (!api?.clean_excel_files) {
ElMessage.warning('当前环境不支持数据去重,请在桌面端打开')
return
}
if (!cleanSelectedPaths.value.length) {
ElMessage.warning('请先选择待清洗 Excel 文件或文件夹') ElMessage.warning('请先选择待清洗 Excel 文件或文件夹')
return return
} }
@@ -271,24 +271,14 @@ async function submitCleanRun() {
try { try {
cleanRunning.value = true cleanRunning.value = true
const result = await api.clean_excel_files({ const result = await runDedupe({
paths: cleanSelectedPaths.value, files: cleanUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
selected_columns: cleanSelectedColumns.value, selectedColumns: cleanSelectedColumns.value,
keep_integer_ids: cleanKeepIntegerIds.value, keepIntegerIds: cleanKeepIntegerIds.value,
keep_underscore_ids: cleanKeepUnderscoreIds.value, keepUnderscoreIds: cleanKeepUnderscoreIds.value,
}) })
cleanSummary.value = result
if (!result.success) { cleanResultItems.value = result.items || []
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,
}
await loadCleanHistory() await loadCleanHistory()
ElMessage.success('数据去重完成') ElMessage.success('数据去重完成')
} catch (error) { } catch (error) {
@@ -298,60 +288,41 @@ async function submitCleanRun() {
} }
} }
onMounted(() => {
loadCleanHistory().catch(() => undefined)
})
async function loadCleanHistory() { async function loadCleanHistory() {
const api = getPywebviewApi()
if (!api?.get_dedupe_history) {
return
}
try { try {
const response = await api.get_dedupe_history() const response = await getDedupeHistory()
if (!response.success || !response.items) return cleanResultItems.value = response.items || []
cleanResultItems.value = response.items
cleanSummary.value = { cleanSummary.value = {
total: response.items.length, total: response.items?.length || 0,
success_count: response.items.filter((item) => item.success).length, successCount: response.items?.filter((item) => item.success).length || 0,
failed_count: response.items.filter((item) => !item.success).length, failedCount: response.items?.filter((item) => !item.success).length || 0,
items: response.items || [],
} }
} catch { } catch {
// ignore history load errors // ignore history load errors
} }
} }
async function deleteCleanHistory(resultId: number) { async function deleteCleanHistoryRecord(resultId: number) {
const api = getPywebviewApi() try {
if (!api?.delete_history_item) { await deleteDedupeHistory(resultId)
ElMessage.warning('当前环境不支持删除历史') await loadCleanHistory()
return ElMessage.success('已删除')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '删除失败')
} }
const result = await api.delete_history_item('dedupe', resultId)
if (!result.success) {
ElMessage.error(result.error || '删除失败')
return
}
await loadCleanHistory()
ElMessage.success('已删除')
} }
async function downloadCleanResult(item: CleanExcelResultItem) { async function downloadCleanResult(item: DedupeResultItem) {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!item.output_path) { if (!item.downloadUrl) {
ElMessage.warning('当前结果没有下载地址') ElMessage.warning('当前结果没有下载地址')
return return
} }
const today = new Date() const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_cleaned.xlsx`
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`
if (api?.save_file_from_url) { 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) { if (result.success) {
ElMessage.success(`已保存:${result.path || filename}`) ElMessage.success(`已保存:${result.path || filename}`)
} else if (result.error && result.error !== '用户取消') { } else if (result.error && result.error !== '用户取消') {
@@ -360,12 +331,17 @@ async function downloadCleanResult(item: CleanExcelResultItem) {
return return
} }
window.open(item.output_path, '_blank') window.open(item.downloadUrl, '_blank')
} }
onMounted(() => {
loadCleanHistory().catch(() => undefined)
})
</script> </script>
<style scoped> <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; } .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; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .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; } .upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; } .hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; } .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 { 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.small { padding: 6px 10px; font-size: 12px; }
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; } .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 { 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; } .selected-files span { display: block; margin: 4px 0; word-break: break-all; }
.more-line { color: #b8c1cc; } .more-line { color: #b8c1cc; }
@@ -415,6 +390,8 @@ async function downloadCleanResult(item: CleanExcelResultItem) {
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; } .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 { 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); } .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; } .empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
.clean-placeholder { max-height: none; } .clean-placeholder { max-height: none; }
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; } .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; } .summary-label { font-size: 12px; color: #8d8d8d; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; } .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; } .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; } } @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> </style>

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="main-content"> <div class="page-shell module-page">
<aside class="left-panel"> <BrandTopBar active="split" />
<div class="main-content">
<aside class="left-panel">
<div class="section-title">选择文件</div> <div class="section-title">选择文件</div>
<div class="upload-zone"> <div class="upload-zone">
<div class="hint">选择需要拆分的 Excel 文件或文件夹按规则拆成多个 Excel 文件</div> <div class="hint">选择需要拆分的 Excel 文件或文件夹按规则拆成多个 Excel 文件</div>
@@ -105,11 +108,11 @@
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="summary-label">成功结果</span> <span class="summary-label">成功结果</span>
<strong>{{ splitSummary.success_count }}</strong> <strong>{{ splitSummary.successCount }}</strong>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="summary-label">失败文件</span> <span class="summary-label">失败文件</span>
<strong>{{ splitSummary.failed_count }}</strong> <strong>{{ splitSummary.failedCount }}</strong>
</div> </div>
</div> </div>
@@ -123,11 +126,11 @@
</div> </div>
<ul v-else class="task-list clean-result-list"> <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"> <div class="left">
<span class="id" :title="item.source_path">{{ item.source_path || '-' }}</span> <span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
<div v-if="item.output_path" class="files">结果下载地址已生成</div> <div v-if="item.outputFilename" class="files">输出文件{{ item.outputFilename }}</div>
<div v-if="item.row_count !== undefined" class="time">包含 {{ item.row_count }} 条数据</div> <div v-if="item.rowCount !== undefined" class="time">包含 {{ item.rowCount }} 条数据</div>
<div v-else-if="item.error" class="files">错误信息{{ item.error }}</div> <div v-else-if="item.error" class="files">错误信息{{ item.error }}</div>
</div> </div>
@@ -136,7 +139,7 @@
{{ item.success ? '已完成' : '失败' }} {{ item.success ? '已完成' : '失败' }}
</span> </span>
<button <button
v-if="item.success && item.output_path" v-if="item.success && item.downloadUrl"
type="button" type="button"
class="download" class="download"
@click="downloadSplitResult(item)" @click="downloadSplitResult(item)"
@@ -144,10 +147,10 @@
下载文件 下载文件
</button> </button>
<button <button
v-if="item.result_id" v-if="item.resultId"
type="button" type="button"
class="btn-delete" class="btn-delete"
@click="deleteSplitHistory(item.result_id)" @click="deleteSplitHistoryRecord(item.resultId)"
> >
删除 删除
</button> </button>
@@ -157,16 +160,20 @@
</div> </div>
</div> </div>
</section> </section>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import { expandBrandFolder } from '@/shared/api/brand' 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 splitSelectedPaths = ref<string[]>([])
const splitUploadedFiles = ref<UploadedJavaFile[]>([])
const splitAvailableColumns = ref<string[]>([]) const splitAvailableColumns = ref<string[]>([])
const splitSelectedColumns = ref<string[]>([]) const splitSelectedColumns = ref<string[]>([])
const splitTotalRows = ref(0) 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 splitRowsPerFile = ref<number | null>(2000)
const splitParts = ref<number | null>(5) const splitParts = ref<number | null>(5)
const splitRunning = ref(false) const splitRunning = ref(false)
const splitOutputDir = ref('') const splitResultItems = ref<SplitResultItem[]>([])
const splitResultItems = ref<SplitExcelResultItem[]>([]) const splitSummary = ref<SplitRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
const splitSummary = ref({ total: 0, success_count: 0, failed_count: 0 })
const splitDisplayPaths = computed(() => splitSelectedPaths.value.slice(0, 8)) const splitDisplayPaths = computed(() => splitSelectedPaths.value.slice(0, 8))
function shorten(value: string, maxLength: number) { async function uploadPathsToJava(paths: string[]) {
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) {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.get_excel_info) { if (!api?.upload_file_to_java) {
ElMessage.warning('当前环境不支持读取 Excel 信息,请在桌面端打开') throw new Error('当前桌面端未提供文件上传桥接能力')
return
} }
const uploaded: UploadedJavaFile[] = []
resetSplitResults() for (const path of paths) {
const result = await api.get_excel_info(filePath) const result = await api.upload_file_to_java(path)
if (!result.success) { if (!result?.success || !result.data) {
ElMessage.error(result.error || '读取 Excel 信息失败') throw new Error(result?.error || result?.message || `上传失败:${path}`)
splitAvailableColumns.value = [] }
splitSelectedColumns.value = [] uploaded.push(result.data)
splitTotalRows.value = 0
return
} }
return uploaded
}
async function loadSplitInfo(fileKey: string) {
const result = await getExcelInfo(fileKey)
splitAvailableColumns.value = result.headers || [] splitAvailableColumns.value = result.headers || []
splitSelectedColumns.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() { async function selectSplitFiles() {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.select_clean_xlsx_files) { if (!api?.select_brand_xlsx_files) {
ElMessage.warning('当前环境不支持文件选择,请在桌面端打开') ElMessage.warning('当前环境不支持文件选择,请在本机客户端中打开')
return return
} }
try { try {
const paths = await api.select_clean_xlsx_files() const paths = await api.select_brand_xlsx_files()
if (!paths?.length) return if (!paths?.length) return
splitSelectedPaths.value = paths await handleSelectedPaths(paths, `已选择 ${paths.length} 个待拆分文件`)
await loadSplitInfo(paths[0])
ElMessage.success(`已选择 ${paths.length} 个待拆分文件`)
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '选择失败') ElMessage.error(error instanceof Error ? error.message : '选择失败')
} }
@@ -230,20 +235,18 @@ async function selectSplitFiles() {
async function selectSplitFolder() { async function selectSplitFolder() {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!api?.select_clean_folder) { if (!api?.select_brand_folder) {
ElMessage.warning('当前环境不支持文件夹选择,请在桌面端打开') ElMessage.warning('当前环境不支持文件夹选择,请在本机客户端中打开')
return return
} }
try { try {
const folder = await api.select_clean_folder() const folder = await api.select_brand_folder()
if (!folder) return if (!folder) return
const result = await expandBrandFolder(folder) const result = await expandBrandFolder(folder)
if (result.success && result.paths?.length) { if (result.success && result.paths?.length) {
splitSelectedPaths.value = result.paths await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
await loadSplitInfo(result.paths[0])
ElMessage.success(`已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
return return
} }
@@ -254,12 +257,7 @@ async function selectSplitFolder() {
} }
async function submitSplitRun() { async function submitSplitRun() {
const api = getPywebviewApi() if (!splitUploadedFiles.value.length) {
if (!api?.split_excel_files) {
ElMessage.warning('当前环境不支持数据拆分,请在桌面端打开')
return
}
if (!splitSelectedPaths.value.length) {
ElMessage.warning('请先选择待拆分 Excel 文件或文件夹') ElMessage.warning('请先选择待拆分 Excel 文件或文件夹')
return return
} }
@@ -278,25 +276,15 @@ async function submitSplitRun() {
try { try {
splitRunning.value = true splitRunning.value = true
const result = await api.split_excel_files({ const result = await runSplit({
paths: splitSelectedPaths.value, files: splitUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
selected_columns: splitSelectedColumns.value, selectedColumns: splitSelectedColumns.value,
split_mode: splitMode.value, splitMode: splitMode.value,
rows_per_file: splitRowsPerFile.value || undefined, rowsPerFile: splitMode.value === 'rows_per_file' ? splitRowsPerFile.value || undefined : undefined,
parts: splitParts.value || undefined, parts: splitMode.value === 'parts' ? splitParts.value || undefined : undefined,
}) })
splitSummary.value = result
if (!result.success) { splitResultItems.value = result.items || []
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,
}
await loadSplitHistory() await loadSplitHistory()
ElMessage.success('数据拆分完成') ElMessage.success('数据拆分完成')
} catch (error) { } catch (error) {
@@ -307,57 +295,40 @@ async function submitSplitRun() {
} }
async function loadSplitHistory() { async function loadSplitHistory() {
const api = getPywebviewApi()
if (!api?.get_split_history) return
try { try {
const response = await api.get_split_history() const response = await getSplitHistory()
if (!response.success || !response.items) return splitResultItems.value = response.items || []
splitResultItems.value = response.items
splitSummary.value = { splitSummary.value = {
total: response.items.length, total: response.items?.length || 0,
success_count: response.items.filter((item) => item.success).length, successCount: response.items?.filter((item) => item.success).length || 0,
failed_count: response.items.filter((item) => !item.success).length, failedCount: response.items?.filter((item) => !item.success).length || 0,
items: response.items || [],
} }
} catch { } catch {
// ignore history load errors // ignore history load errors
} }
} }
onMounted(() => { async function deleteSplitHistoryRecord(resultId: number) {
loadSplitHistory().catch(() => undefined) try {
}) await deleteSplitHistory(resultId)
await loadSplitHistory()
async function deleteSplitHistory(resultId: number) { ElMessage.success('已删除')
const api = getPywebviewApi() } catch (error) {
if (!api?.delete_history_item) { ElMessage.error(error instanceof Error ? error.message : '删除失败')
ElMessage.warning('当前环境不支持删除历史')
return
} }
const result = await api.delete_history_item('split', resultId)
if (!result.success) {
ElMessage.error(result.error || '删除失败')
return
}
await loadSplitHistory()
ElMessage.success('已删除')
} }
async function downloadSplitResult(item: SplitExcelResultItem) { async function downloadSplitResult(item: SplitResultItem) {
const api = getPywebviewApi() const api = getPywebviewApi()
if (!item.output_path) { if (!item.downloadUrl) {
ElMessage.warning('当前结果没有下载地址') ElMessage.warning('当前结果没有下载地址')
return return
} }
const today = new Date() const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.xlsx`
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`
if (api?.save_file_from_url) { 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) { if (result.success) {
ElMessage.success(`已保存:${result.path || filename}`) ElMessage.success(`已保存:${result.path || filename}`)
} else if (result.error && result.error !== '用户取消') { } else if (result.error && result.error !== '用户取消') {
@@ -366,12 +337,17 @@ async function downloadSplitResult(item: SplitExcelResultItem) {
return return
} }
window.open(item.output_path, '_blank') window.open(item.downloadUrl, '_blank')
} }
onMounted(() => {
loadSplitHistory().catch(() => undefined)
})
</script> </script>
<style scoped> <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; } .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; } .right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .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; } .upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
.hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; } .hint { color: #888; font-size: 13px; margin-bottom: 12px; line-height: 1.5; }
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; } .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 { 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.small { padding: 6px 10px; font-size: 12px; }
.opt-btn:hover { color: #3498db; background: #333; border-color: #3498db; } .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; } .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 { 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); } .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; } .empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
.clean-placeholder { max-height: none; } .clean-placeholder { max-height: none; }
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; } .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; } .summary-label { font-size: 12px; color: #8d8d8d; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; } .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; } .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; } } @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> </style>

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

View File

@@ -39,48 +39,50 @@ export interface BrandTaskMutationResponse {
error?: string error?: string
} }
const API_PREFIX = '/new'
export function getBrandTaskEventsUrl(taskId: string | number) { 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) { export function getBrandTaskDownloadUrl(taskId: string | number) {
return `/api/brand/download/${taskId}` return `${API_PREFIX}/api/brand/download/${taskId}`
} }
export function getBrandTemplateXlsxUrl() { export function getBrandTemplateXlsxUrl() {
return '/static/品牌文档格式_模板.xlsx' return `${API_PREFIX}/static/品牌文档格式_模板.xlsx`
} }
export function getBrandTemplateZipUrl() { export function getBrandTemplateZipUrl() {
return '/static/模板2-以文件夹方式上传.zip' return `${API_PREFIX}/static/模板2-以文件夹方式上传.zip`
} }
export function expandBrandFolder(folder: string) { 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) { 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) { 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() { export function getBrandTasks() {
return requestGetJson<BrandTaskListResponse>('/api/brand/tasks') return requestGetJson<BrandTaskListResponse>(`${API_PREFIX}/api/brand/tasks`)
} }
export function getBrandTask(taskId: string | number) { 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) { 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) { 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) { export function createBrandTaskEvents(taskId: string | number) {

View File

@@ -6,26 +6,36 @@ import axios, {
type InternalAxiosRequestConfig, type InternalAxiosRequestConfig,
} from 'axios' } from 'axios'
export interface ApiSuccess<T> { export interface LegacyApiSuccess<T> {
success: true success: true
msg?: string msg?: string
data?: T data?: T
} }
export interface ApiFailure { export interface LegacyApiFailure {
success: false success: false
error?: string 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> export type RequestOptions<D = unknown> = AxiosRequestConfig<D>
function extractErrorMessage(error: unknown) { function extractErrorMessage(error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
const data = error.response?.data const data = error.response?.data
if (data && typeof data === 'object' && 'error' in data) { if (data && typeof data === 'object') {
return String(data.error) 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()) { if (typeof data === 'string' && data.trim()) {
@@ -105,3 +115,11 @@ export const requestGetJson = get
export const requestPostJson = post export const requestPostJson = post
export const requestPutJson = put export const requestPutJson = put
export const requestDeleteJson = del 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
}

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

View File

@@ -1,149 +1,24 @@
export interface SplitExcelPayload { export interface UploadedJavaFile {
paths: string[] fileKey: string
selected_columns: string[] originalFilename: string
split_mode: 'rows_per_file' | 'parts' localPath: string
rows_per_file?: number size: 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 PywebviewApi { export interface PywebviewApi {
select_brand_xlsx_files?: () => Promise<string[]> close?: () => Promise<void>
select_clean_xlsx_files?: () => Promise<string[]> minimize?: () => Promise<void>
select_brand_folder?: () => Promise<string | null> maximize?: () => Promise<void>
select_clean_folder?: () => Promise<string | null> 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> select_folder?: () => Promise<string | null>
get_excel_headers?: (filePath: string) => Promise<{ success: boolean; headers?: string[]; error?: string }> select_brand_xlsx_files?: () => Promise<string[]>
get_dedupe_history?: () => Promise<CleanExcelHistoryResponse> select_brand_folder?: () => Promise<string | null>
get_convert_history?: () => Promise<ConvertTxtHistoryResponse> upload_file_to_java?: (filePath: string) => Promise<{ success: boolean; message?: string; data?: UploadedJavaFile; error?: string }>
get_split_history?: () => Promise<SplitExcelHistoryResponse> save_file_from_url?: (url: string, filename: string) => Promise<{ success: boolean; path?: string; error?: string }>
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 }>
save_template_xlsx?: () => 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_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 { 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() { export function getPywebviewApi() {
return window.pywebview?.api return syncPywebviewApi() || cachedPywebviewApi
} }
export function hasPywebview() { export function hasPywebview() {

View File

@@ -7,7 +7,7 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({ export default defineConfig({
base: './', base: '/',
plugins: [ plugins: [
vue(), vue(),
AutoImport({ AutoImport({
@@ -29,26 +29,15 @@ export default defineConfig({
server: { server: {
port: 5173, port: 5173,
proxy: { proxy: {
'/api': { '/newApi/api': {
target: 'http://127.0.0.1:8000', target: 'http://127.0.0.1:18080',
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',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/newApi/, ''),
}, },
}, },
}, },
build: { build: {
outDir: 'dist', outDir: 'new_web_source',
emptyOutDir: true, emptyOutDir: true,
cssCodeSplit: true, cssCodeSplit: true,
rollupOptions: { rollupOptions: {

Binary file not shown.

Binary file not shown.

Binary file not shown.