环境搭建配置
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,6 +3,7 @@ frontend-vue/node_modules/
|
|||||||
|
|
||||||
# frontend-vue build output
|
# frontend-vue 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/
|
||||||
|
|||||||
12
app/.env
Normal file
12
app/.env
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
base_url=http://159.75.121.33:15124
|
||||||
|
workflow_id=7608812635877900322
|
||||||
|
mysql_host=159.75.121.33
|
||||||
|
mysql_user=AIimage
|
||||||
|
|
||||||
|
proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=r4m8dov52giup2o
|
||||||
|
proxy_mode=2
|
||||||
|
|
||||||
|
client_name=NanriAI
|
||||||
|
java_api_base=http://127.0.0.1:18080
|
||||||
|
|
||||||
|
|
||||||
BIN
app/__pycache__/app.cpython-312.pyc
Normal file
BIN
app/__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/app_common.cpython-312.pyc
Normal file
BIN
app/__pycache__/app_common.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/html_crypto.cpython-312.pyc
Normal file
BIN
app/__pycache__/html_crypto.cpython-312.pyc
Normal file
Binary file not shown.
@@ -37,7 +37,7 @@ app = create_app()
|
|||||||
|
|
||||||
|
|
||||||
def run_app(host='127.0.0.1', port=5123):
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
1
app/assets/convert-D4Yv2oRy.css
Normal file
1
app/assets/convert-D4Yv2oRy.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert.js
Normal file
1
app/assets/convert.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-BHE_BCAJ.css
Normal file
1
app/assets/dedupe-BHE_BCAJ.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe.js
Normal file
1
app/assets/dedupe.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/pywebview-Chz98E9c.css
Normal file
1
app/assets/pywebview-Chz98E9c.css
Normal file
File diff suppressed because one or more lines are too long
55
app/assets/pywebview-Rm5Hhqpf.js
Normal file
55
app/assets/pywebview-Rm5Hhqpf.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/split-_O6NHqDj.css
Normal file
1
app/assets/split-_O6NHqDj.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/split.js
Normal file
1
app/assets/split.js
Normal file
File diff suppressed because one or more lines are too long
BIN
app/blueprints/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/admin.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/auth.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/brand.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/brand.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/image.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/image.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/blueprints/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/blueprints/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
@@ -2,7 +2,8 @@
|
|||||||
主页面蓝图:首页、home、图片工作台、品牌页、静态文件、Logo
|
主页面蓝图:首页、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')
|
||||||
|
|||||||
BIN
app/brand_spider/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/brand_spider/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/brand_spider/__pycache__/web_dec.cpython-312.pyc
Normal file
BIN
app/brand_spider/__pycache__/web_dec.cpython-312.pyc
Normal file
Binary file not shown.
@@ -7,6 +7,7 @@ import os
|
|||||||
from dotenv import load_dotenv
|
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"
|
||||||
|
|||||||
28
app/main.py
28
app/main.py
@@ -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,
|
||||||
|
|||||||
15
app/new_web_source/convert.html
Normal file
15
app/new_web_source/convert.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>格式转换 - 数富AI</title>
|
||||||
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-Rm5Hhqpf.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Chz98E9c.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/convert-D4Yv2oRy.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
app/new_web_source/dedupe.html
Normal file
15
app/new_web_source/dedupe.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>数据去重 - 数富AI</title>
|
||||||
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-Rm5Hhqpf.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Chz98E9c.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-BHE_BCAJ.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
app/new_web_source/split.html
Normal file
15
app/new_web_source/split.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>数据拆分 - 数富AI</title>
|
||||||
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-Rm5Hhqpf.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-Chz98E9c.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/split-_O6NHqDj.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
app/tool/__pycache__/devices.cpython-312.pyc
Normal file
BIN
app/tool/__pycache__/devices.cpython-312.pyc
Normal file
Binary file not shown.
1
app/version.txt
Normal file
1
app/version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version": "1.0.3"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,11 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.model.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "Excel 文件信息")
|
||||||
|
public class ExcelInfoVo {
|
||||||
|
|
||||||
|
@Schema(description = "表头列表")
|
||||||
|
private List<String> headers;
|
||||||
|
|
||||||
|
@Schema(description = "数据总行数")
|
||||||
|
private Integer totalRows;
|
||||||
|
}
|
||||||
@@ -2,14 +2,26 @@ package com.nanri.aiimage.modules.file.service;
|
|||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.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+", " ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
137
frontend-vue/src/pages/brand/components/BrandTopBar.vue
Normal file
137
frontend-vue/src/pages/brand/components/BrandTopBar.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<header class="top-bar">
|
||||||
|
<div class="logo-area">
|
||||||
|
<span class="app-name">数富AI-亚马逊</span>
|
||||||
|
<a href="/home" class="btn-home">返回首页</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-tabs">
|
||||||
|
<span class="nav-tab-group">
|
||||||
|
<a v-for="item in navItems" :key="item.key" :href="item.href" class="nav-tab" :class="{ active: active === item.key }">
|
||||||
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="top-right"></div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
active: 'brand' | 'dedupe' | 'convert' | 'split'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const active = props.active
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ key: 'brand', label: '品牌检测', href: '/brand' },
|
||||||
|
{ key: 'dedupe', label: '数据去重', href: '/new_web_source/dedupe.html' },
|
||||||
|
{ key: 'convert', label: '格式转换', href: '/new_web_source/convert.html' },
|
||||||
|
{ key: 'split', label: '数据拆分', href: '/new_web_source/split.html' },
|
||||||
|
] as const
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.top-bar {
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: #111;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-home {
|
||||||
|
color: #d2d2d2;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-home:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 92px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: #8d8d8d;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab:hover {
|
||||||
|
color: #b9d6ff;
|
||||||
|
background: rgba(77, 126, 189, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab.active {
|
||||||
|
background: rgba(77, 126, 189, 0.24);
|
||||||
|
border-color: rgba(91, 148, 219, 0.35);
|
||||||
|
color: #6caeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-right {
|
||||||
|
width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.top-bar {
|
||||||
|
height: auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-right {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -39,48 +39,50 @@ export interface BrandTaskMutationResponse {
|
|||||||
error?: string
|
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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
162
frontend-vue/src/shared/api/java-modules.ts
Normal file
162
frontend-vue/src/shared/api/java-modules.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { del, get, http, post, type JavaApiResponse, unwrapJavaResponse } from '@/shared/api/http'
|
||||||
|
|
||||||
|
const JAVA_API_PREFIX = '/newApi/api'
|
||||||
|
|
||||||
|
export interface UploadedFileRef {
|
||||||
|
fileKey: string
|
||||||
|
originalFilename?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadFileVo {
|
||||||
|
fileKey: string
|
||||||
|
originalFilename: string
|
||||||
|
localPath: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DedupeResultItem {
|
||||||
|
resultId?: number
|
||||||
|
sourceFilename: string
|
||||||
|
outputFilename?: string
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
downloadUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DedupeRunVo {
|
||||||
|
total: number
|
||||||
|
successCount: number
|
||||||
|
failedCount: number
|
||||||
|
items: DedupeResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DedupeHistoryVo {
|
||||||
|
items: DedupeResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DedupeRunRequest {
|
||||||
|
files: UploadedFileRef[]
|
||||||
|
selectedColumns: string[]
|
||||||
|
keepIntegerIds: boolean
|
||||||
|
keepUnderscoreIds: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitResultItem {
|
||||||
|
resultId?: number
|
||||||
|
sourceFilename: string
|
||||||
|
outputFilename?: string
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
rowCount?: number
|
||||||
|
downloadUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitRunVo {
|
||||||
|
total: number
|
||||||
|
successCount: number
|
||||||
|
failedCount: number
|
||||||
|
items: SplitResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitHistoryVo {
|
||||||
|
items: SplitResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitRunRequest {
|
||||||
|
files: UploadedFileRef[]
|
||||||
|
selectedColumns: string[]
|
||||||
|
splitMode: 'rows_per_file' | 'parts'
|
||||||
|
rowsPerFile?: number
|
||||||
|
parts?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConvertResultItem {
|
||||||
|
resultId?: number
|
||||||
|
sourceFilename: string
|
||||||
|
outputFilename?: string
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
downloadUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConvertRunVo {
|
||||||
|
total: number
|
||||||
|
successCount: number
|
||||||
|
failedCount: number
|
||||||
|
items: ConvertResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConvertHistoryVo {
|
||||||
|
items: ConvertResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConvertRunRequest {
|
||||||
|
files: UploadedFileRef[]
|
||||||
|
templateId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConvertTemplateVo {
|
||||||
|
id: string
|
||||||
|
templateCode: string
|
||||||
|
templateName: string
|
||||||
|
outputFilename: string
|
||||||
|
isDefault?: boolean
|
||||||
|
builtIn?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExcelInfoVo {
|
||||||
|
headers: string[]
|
||||||
|
totalRows: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExcelInfo(fileKey: string) {
|
||||||
|
return unwrapJavaResponse(get<JavaApiResponse<ExcelInfoVo>>(`${JAVA_API_PREFIX}/files/excel-info`, {
|
||||||
|
params: { fileKey },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runDedupe(request: DedupeRunRequest) {
|
||||||
|
return unwrapJavaResponse(post<JavaApiResponse<DedupeRunVo>, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDedupeHistory() {
|
||||||
|
return unwrapJavaResponse(get<JavaApiResponse<DedupeHistoryVo>>(`${JAVA_API_PREFIX}/dedupe/history`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDedupeHistory(resultId: number) {
|
||||||
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runSplit(request: SplitRunRequest) {
|
||||||
|
return unwrapJavaResponse(post<JavaApiResponse<SplitRunVo>, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSplitHistory() {
|
||||||
|
return unwrapJavaResponse(get<JavaApiResponse<SplitHistoryVo>>(`${JAVA_API_PREFIX}/split/history`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteSplitHistory(resultId: number) {
|
||||||
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/split/history/${resultId}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConvertTemplates() {
|
||||||
|
return unwrapJavaResponse(get<JavaApiResponse<ConvertTemplateVo[]>>(`${JAVA_API_PREFIX}/convert/templates`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runConvert(request: ConvertRunRequest) {
|
||||||
|
return unwrapJavaResponse(post<JavaApiResponse<ConvertRunVo>, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConvertHistory() {
|
||||||
|
return unwrapJavaResponse(get<JavaApiResponse<ConvertHistoryVo>>(`${JAVA_API_PREFIX}/convert/history`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteConvertHistory(resultId: number) {
|
||||||
|
return unwrapJavaResponse(del<JavaApiResponse<null>>(`${JAVA_API_PREFIX}/convert/history/${resultId}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJavaDownloadUrl(path: string) {
|
||||||
|
return path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export { JAVA_API_PREFIX, http }
|
||||||
@@ -1,149 +1,24 @@
|
|||||||
export interface SplitExcelPayload {
|
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() {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
BIN
source_code/__pycache__/app.cpython-314.pyc
Normal file
BIN
source_code/__pycache__/app.cpython-314.pyc
Normal file
Binary file not shown.
BIN
source_code/__pycache__/config.cpython-314.pyc
Normal file
BIN
source_code/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
source_code/__pycache__/main.cpython-314.pyc
Normal file
BIN
source_code/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user