增加公共下载进度、增加接收SKU、密钥分别存放

This commit is contained in:
super
2026-05-28 16:41:20 +08:00
parent ca4a2cd07a
commit 2ed1250604
119 changed files with 4077 additions and 293 deletions

View File

@@ -12,8 +12,8 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
client_name=ShuFuAI
# java_api_base=http://47.111.163.154:18080
# java_api_base=http://127.0.0.1:18080
java_api_base=http://121.196.149.225:18080
java_api_base=http://127.0.0.1:18080
# java_api_base=http://121.196.149.225:18080
# 与 Java 后端共享的 JWT 签名密钥,必须与 backend-java 的 AIIMAGE_JWT_SECRET 完全一致
AIIMAGE_JWT_SECRET=please-change-this-secret-please-rotate-at-least-32-bytes

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{b8 as r}from"./pywebview-3bFwh9Cg.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};

View File

@@ -0,0 +1 @@
import{bN as r}from"./java-modules-DzYrjxib.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};

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

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

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

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

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

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.module-page[data-v-a27e815e]{min-height:100vh;background:#1a1a1a}.main-content[data-v-a27e815e]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-a27e815e]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-a27e815e]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-a27e815e],.subsection-title[data-v-a27e815e]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-a27e815e]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-a27e815e],.loading-msg[data-v-a27e815e],.files[data-v-a27e815e],.muted[data-v-a27e815e]{color:#888;font-size:12px;line-height:1.5}.link[data-v-a27e815e]{color:#6ea8fe;text-decoration:none}.link[data-v-a27e815e]:hover{color:#9fc5ff}.btns[data-v-a27e815e],.run-row[data-v-a27e815e]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-a27e815e],.btn-run[data-v-a27e815e],.btn-delete[data-v-a27e815e],.download[data-v-a27e815e]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-a27e815e]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-a27e815e]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-a27e815e]{background:#27ae60}.btn-run[data-v-a27e815e]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-a27e815e]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-a27e815e]{display:block;margin:4px 0}.parse-card[data-v-a27e815e],.queue-payload[data-v-a27e815e]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-a27e815e]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-a27e815e]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-a27e815e]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-a27e815e]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-a27e815e]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-a27e815e]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-a27e815e]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-a27e815e]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-a27e815e]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-a27e815e]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-a27e815e]{list-style:none;margin:0;padding:12px}.task-item[data-v-a27e815e]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-a27e815e]{flex:1;min-width:0}.id[data-v-a27e815e]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-a27e815e]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-a27e815e]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-a27e815e]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-a27e815e]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-a27e815e]{background:#3498db2e;color:#3498db}.status.pending[data-v-a27e815e]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-a27e815e]{margin-top:6px;color:#e0b96d}.file-progress[data-v-a27e815e]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-a27e815e]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-a27e815e]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-a27e815e]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-a27e815e]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-a27e815e]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-a27e815e]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-a27e815e]{flex-direction:column;height:auto}.left-panel[data-v-a27e815e]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-a27e815e]{grid-template-columns:repeat(2,minmax(0,1fr))}}

View File

@@ -0,0 +1 @@
.module-page[data-v-39de0e76]{min-height:100vh;background:#1a1a1a}.main-content[data-v-39de0e76]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-39de0e76]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-39de0e76]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-39de0e76],.subsection-title[data-v-39de0e76]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-39de0e76]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-39de0e76],.loading-msg[data-v-39de0e76],.files[data-v-39de0e76],.muted[data-v-39de0e76]{color:#888;font-size:12px;line-height:1.5}.link[data-v-39de0e76]{color:#6ea8fe;text-decoration:none}.link[data-v-39de0e76]:hover{color:#9fc5ff}.btns[data-v-39de0e76],.run-row[data-v-39de0e76]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-39de0e76],.btn-run[data-v-39de0e76],.btn-delete[data-v-39de0e76],.download[data-v-39de0e76]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-39de0e76]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-39de0e76]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-39de0e76]{background:#27ae60}.btn-run[data-v-39de0e76]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-39de0e76]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-39de0e76]{display:block;margin:4px 0}.parse-card[data-v-39de0e76],.queue-payload[data-v-39de0e76]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-39de0e76]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-39de0e76]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-39de0e76]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-39de0e76]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-39de0e76]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-39de0e76]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-39de0e76]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-39de0e76]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-39de0e76]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-39de0e76]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-39de0e76]{list-style:none;margin:0;padding:12px}.task-item[data-v-39de0e76]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-39de0e76]{flex:1;min-width:0}.id[data-v-39de0e76]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-39de0e76]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-39de0e76]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-39de0e76]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-39de0e76]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-39de0e76]{background:#3498db2e;color:#3498db}.status.pending[data-v-39de0e76]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-39de0e76]{margin-top:6px;color:#e0b96d}.file-progress[data-v-39de0e76]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-39de0e76]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-39de0e76]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-39de0e76]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-39de0e76]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-39de0e76]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-39de0e76]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-39de0e76]{flex-direction:column;height:auto}.left-panel[data-v-39de0e76]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-39de0e76]{grid-template-columns:repeat(2,minmax(0,1fr))}}

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

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,8 @@ import json
import sys
import shutil
import os
import uuid
import ctypes
os.makedirs(cache_path,exist_ok=True)
if not debug:
today = datetime.datetime.now().strftime("%Y_%m_%d")
@@ -186,17 +188,7 @@ class WindowAPI:
if not result:
return {'success': False, 'error': '用户取消'}
path = result[0] if isinstance(result, (list, tuple)) else result
try:
import requests
resp = requests.get(url.strip(), timeout=120, stream=True)
resp.raise_for_status()
with open(path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=65536):
if chunk:
f.write(chunk)
return {'success': True, 'path': path}
except Exception as e:
return {'success': False, 'error': str(e)}
return self._download_url_to_path(url, path)
def save_file_from_url_new(self, url, default_filename='download.bin'):
"""弹窗选择保存位置,从 url 下载文件并保存。根据文件后缀动态设置保存类型。"""
@@ -230,18 +222,124 @@ class WindowAPI:
return {'success': False, 'error': '用户取消'}
path = result[0] if isinstance(result, (list, tuple)) else result
return self._download_url_to_path(url, path)
def save_file_from_url_with_progress(self, url, default_filename='download.bin', download_id=''):
"""弹窗选择保存位置,从 url 下载文件并通过前端事件上报下载进度。"""
print("调用到save_file_from_url_with_progress方法")
if not url or not url.strip():
return {'success': False, 'error': '下载地址为空'}
filename = str(default_filename or 'download.bin').strip() or 'download.bin'
ext = os.path.splitext(filename)[1].lower()
if ext == '.zip':
file_types = ('ZIP 压缩包 (*.zip)', '所有文件 (*.*)')
elif ext in ('.xlsx', '.xls'):
file_types = ('Excel 文件 (*.xlsx;*.xls)', '所有文件 (*.*)')
elif ext == '.csv':
file_types = ('CSV 文件 (*.csv)', '所有文件 (*.*)')
elif ext == '.txt':
file_types = ('文本文件 (*.txt)', '所有文件 (*.*)')
elif ext == '.json':
file_types = ('JSON 文件 (*.json)', '所有文件 (*.*)')
else:
file_types = ('所有文件 (*.*)',)
result = self._window.create_file_dialog(
webview.SAVE_DIALOG,
save_filename=filename,
file_types=file_types
)
if not result:
return {'success': False, 'error': '用户取消'}
path = result[0] if isinstance(result, (list, tuple)) else result
progress_id = str(download_id or uuid.uuid4())
return self._download_url_to_path(url, path, progress_id)
def _download_url_to_path(self, url, path, download_id=''):
"""先下载到临时文件,完整下载后再替换为最终文件,避免用户打开半成品。"""
final_path = os.path.abspath(path)
final_dir = os.path.dirname(final_path) or os.getcwd()
final_name = os.path.basename(final_path)
temp_path = os.path.join(final_dir, f".{final_name}.crdownload")
last_progress_emit = 0.0
last_progress_percent = -1
try:
import requests
os.makedirs(final_dir, exist_ok=True)
if os.path.exists(temp_path):
os.remove(temp_path)
resp = requests.get(url.strip(), timeout=120, stream=True)
resp.raise_for_status()
with open(path, 'wb') as f:
total = int(resp.headers.get('Content-Length') or 0)
downloaded = 0
self._emit_download_progress(download_id, 'running', final_path, downloaded, total)
with open(temp_path, 'wb') as f:
self._hide_file(temp_path)
for chunk in resp.iter_content(chunk_size=65536):
if chunk:
f.write(chunk)
return {'success': True, 'path': path}
downloaded += len(chunk)
percent = int(downloaded * 100 / total) if total else 0
now_time = time.time()
if percent != last_progress_percent or now_time - last_progress_emit >= 0.2:
self._emit_download_progress(download_id, 'running', final_path, downloaded, total)
last_progress_emit = now_time
last_progress_percent = percent
f.flush()
os.fsync(f.fileno())
self._show_file(temp_path)
os.replace(temp_path, final_path)
self._emit_download_progress(download_id, 'success', final_path, downloaded, total)
return {'success': True, 'path': final_path}
except Exception as e:
try:
if os.path.exists(temp_path):
os.remove(temp_path)
except Exception:
pass
self._emit_download_progress(download_id, 'failed', final_path, 0, 0, str(e))
return {'success': False, 'error': str(e)}
def _emit_download_progress(self, download_id, status, path, downloaded, total, error=''):
if not download_id:
return
payload = {
'id': download_id,
'status': status,
'path': path,
'downloaded': downloaded,
'total': total,
'percent': round(downloaded * 100 / total, 1) if total else 0,
'error': error,
}
script = (
"window.dispatchEvent(new CustomEvent('pywebview-download-progress', "
f"{{ detail: {json.dumps(payload, ensure_ascii=False)} }}));"
)
try:
self._window.evaluate_js(script)
except Exception:
pass
def _hide_file(self, path):
"""Windows 下隐藏临时下载文件,减少用户误打开半成品的机会。"""
if os.name != 'nt':
return
try:
ctypes.windll.kernel32.SetFileAttributesW(path, 0x02)
except Exception:
pass
def _show_file(self, path):
"""恢复普通文件属性,避免最终保存文件被隐藏。"""
if os.name != 'nt':
return
try:
ctypes.windll.kernel32.SetFileAttributesW(path, 0x80)
except Exception:
pass
def save_template_xlsx(self):
"""弹窗选择保存位置,将品牌文档格式模板 xlsx 保存到用户选择的位置。"""
template_name = '品牌文档格式_模板.xlsx'
@@ -440,7 +538,7 @@ def main():
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,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del,api.save_file_from_url_new,api.get_device_id)
api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del,api.save_file_from_url_new,api.save_file_from_url_with_progress,api.get_device_id)
webview.start(
debug=True,
storage_path=cache_path,

View File

@@ -5,10 +5,10 @@
<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-3bFwh9Cg.js">
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/convert-6oxZNMye.css">
</head>
<body>
<div id="app"></div>

View File

@@ -5,10 +5,10 @@
<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-3bFwh9Cg.js">
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/dedupe-DpfPYQDv.css">
</head>
<body>
<div id="app"></div>

View File

@@ -5,11 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>删除品牌 - 数富AI</title>
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
<link rel="modulepreload" crossorigin href="/assets/pywebview-3bFwh9Cg.js">
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BJod9K65.css">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/delete-brand-DZ3x5hOX.css">
</head>
<body>
<div id="app"></div>

View File

@@ -5,10 +5,10 @@
<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-3bFwh9Cg.js">
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/split-CR1PtDQE.css">
</head>
<body>
<div id="app"></div>